Commit aba9cc6f authored by Sean McGivern's avatar Sean McGivern

Merge branch 'master' into expiration-date-on-memberships

parents 883b96ab 717366d2
...@@ -11,6 +11,7 @@ v 8.11.0 (unreleased) ...@@ -11,6 +11,7 @@ v 8.11.0 (unreleased)
- Rename `markdown_preview` routes to `preview_markdown`. (Christopher Bartz) - Rename `markdown_preview` routes to `preview_markdown`. (Christopher Bartz)
- Update to Ruby 2.3.1. !4948 - Update to Ruby 2.3.1. !4948
- Add Issues Board !5548 - Add Issues Board !5548
- Allow resolving merge conflicts in the UI !5479
- Improve diff performance by eliminating redundant checks for text blobs - Improve diff performance by eliminating redundant checks for text blobs
- Ensure that branch names containing escapable characters (e.g. %20) aren't unescaped indiscriminately. !5770 (ewiltshi) - Ensure that branch names containing escapable characters (e.g. %20) aren't unescaped indiscriminately. !5770 (ewiltshi)
- Convert switch icon into icon font (ClemMakesApps) - Convert switch icon into icon font (ClemMakesApps)
...@@ -20,6 +21,7 @@ v 8.11.0 (unreleased) ...@@ -20,6 +21,7 @@ v 8.11.0 (unreleased)
- Remove magic comments (`# encoding: UTF-8`) from Ruby files. !5456 (winniehell) - Remove magic comments (`# encoding: UTF-8`) from Ruby files. !5456 (winniehell)
- GitLab Performance Monitoring can now track custom events such as the number of tags pushed to a repository - GitLab Performance Monitoring can now track custom events such as the number of tags pushed to a repository
- Add support for relative links starting with ./ or / to RelativeLinkFilter (winniehell) - Add support for relative links starting with ./ or / to RelativeLinkFilter (winniehell)
- Allow naming U2F devices !5833
- Ignore URLs starting with // in Markdown links !5677 (winniehell) - Ignore URLs starting with // in Markdown links !5677 (winniehell)
- Fix CI status icon link underline (ClemMakesApps) - Fix CI status icon link underline (ClemMakesApps)
- The Repository class is now instrumented - The Repository class is now instrumented
...@@ -28,6 +30,8 @@ v 8.11.0 (unreleased) ...@@ -28,6 +30,8 @@ v 8.11.0 (unreleased)
- Expand commit message width in repo view (ClemMakesApps) - Expand commit message width in repo view (ClemMakesApps)
- Cache highlighted diff lines for merge requests - Cache highlighted diff lines for merge requests
- Pre-create all builds for a Pipeline when the new Pipeline is created !5295 - Pre-create all builds for a Pipeline when the new Pipeline is created !5295
- API: Add deployment endpoints
- API: Add Play endpoint on Builds
- Fix of 'Commits being passed to custom hooks are already reachable when using the UI' - Fix of 'Commits being passed to custom hooks are already reachable when using the UI'
- Show member roles to all users on members page - Show member roles to all users on members page
- Project.visible_to_user is instrumented again - Project.visible_to_user is instrumented again
...@@ -51,8 +55,10 @@ v 8.11.0 (unreleased) ...@@ -51,8 +55,10 @@ v 8.11.0 (unreleased)
- Show deployment status on merge requests with external URLs - Show deployment status on merge requests with external URLs
- Clean up unused routes (Josef Strzibny) - Clean up unused routes (Josef Strzibny)
- Fix issue on empty project to allow developers to only push to protected branches if given permission - Fix issue on empty project to allow developers to only push to protected branches if given permission
- API: Add enpoints for pipelines
- Add green outline to New Branch button. !5447 (winniehell) - Add green outline to New Branch button. !5447 (winniehell)
- Optimize generating of cache keys for issues and notes - Optimize generating of cache keys for issues and notes
- Fix repository push email formatting in Outlook
- Improve performance of syntax highlighting Markdown code blocks - Improve performance of syntax highlighting Markdown code blocks
- Update to gitlab_git 10.4.1 and take advantage of preserved Ref objects - Update to gitlab_git 10.4.1 and take advantage of preserved Ref objects
- Remove delay when hitting "Reply..." button on page with a lot of discussions - Remove delay when hitting "Reply..." button on page with a lot of discussions
...@@ -64,6 +70,8 @@ v 8.11.0 (unreleased) ...@@ -64,6 +70,8 @@ v 8.11.0 (unreleased)
- Update version_sorter and use new interface for faster tag sorting - Update version_sorter and use new interface for faster tag sorting
- Optimize checking if a user has read access to a list of issues !5370 - Optimize checking if a user has read access to a list of issues !5370
- Store all DB secrets in secrets.yml, under descriptive names !5274 - Store all DB secrets in secrets.yml, under descriptive names !5274
- Fix syntax highlighting in file editor
- Support slash commands in issue and merge request descriptions as well as comments. !5021
- Nokogiri's various parsing methods are now instrumented - Nokogiri's various parsing methods are now instrumented
- Add archived badge to project list !5798 - Add archived badge to project list !5798
- Add simple identifier to public SSH keys (muteor) - Add simple identifier to public SSH keys (muteor)
...@@ -81,6 +89,8 @@ v 8.11.0 (unreleased) ...@@ -81,6 +89,8 @@ v 8.11.0 (unreleased)
- Remove `search_id` of labels dropdown filter to fix 'Missleading URI for labels in Merge Requests and Issues view'. !5368 (Scott Le) - Remove `search_id` of labels dropdown filter to fix 'Missleading URI for labels in Merge Requests and Issues view'. !5368 (Scott Le)
- Load project invited groups and members eagerly in `ProjectTeam#fetch_members` - Load project invited groups and members eagerly in `ProjectTeam#fetch_members`
- Add pipeline events hook - Add pipeline events hook
- Award emoji tooltips containing more than 10 usernames are now truncated !4780 (jlogandavison)
- Fix duplicate "me" in award emoji tooltip !5218 (jlogandavison)
- Bump gitlab_git to speedup DiffCollection iterations - Bump gitlab_git to speedup DiffCollection iterations
- Rewrite description of a blocked user in admin settings. (Elias Werberich) - Rewrite description of a blocked user in admin settings. (Elias Werberich)
- Make branches sortable without push permission !5462 (winniehell) - Make branches sortable without push permission !5462 (winniehell)
...@@ -103,6 +113,7 @@ v 8.11.0 (unreleased) ...@@ -103,6 +113,7 @@ v 8.11.0 (unreleased)
- Add commit stats in commit api. !5517 (dixpac) - Add commit stats in commit api. !5517 (dixpac)
- Add CI configuration button on project page - Add CI configuration button on project page
- Fix merge request new view not changing code view rendering style - Fix merge request new view not changing code view rendering style
- edit_blob_link will use blob passed onto the options parameter
- Make error pages responsive (Takuya Noguchi) - Make error pages responsive (Takuya Noguchi)
- The performance of the project dropdown used for moving issues has been improved - The performance of the project dropdown used for moving issues has been improved
- Fix skip_repo parameter being ignored when destroying a namespace - Fix skip_repo parameter being ignored when destroying a namespace
...@@ -127,10 +138,13 @@ v 8.11.0 (unreleased) ...@@ -127,10 +138,13 @@ v 8.11.0 (unreleased)
- Sort folders with submodules in Files view !5521 - Sort folders with submodules in Files view !5521
- Each `File::exists?` replaced to `File::exist?` because of deprecate since ruby version 2.2.0 - Each `File::exists?` replaced to `File::exist?` because of deprecate since ruby version 2.2.0
- Add auto-completition in pipeline (Katarzyna Kobierska Ula Budziszewska) - Add auto-completition in pipeline (Katarzyna Kobierska Ula Budziszewska)
- Add pipelines tab to merge requests
- Fix a memory leak caused by Banzai::Filter::SanitizationFilter - Fix a memory leak caused by Banzai::Filter::SanitizationFilter
- Speed up todos queries by limiting the projects set we join with - Speed up todos queries by limiting the projects set we join with
- Ensure file editing in UI does not overwrite commited changes without warning user - Ensure file editing in UI does not overwrite commited changes without warning user
- Eliminate unneeded calls to Repository#blob_at when listing commits with no path - Eliminate unneeded calls to Repository#blob_at when listing commits with no path
- Update gitlab_git gem to 10.4.7
- Simplify SQL queries of marking a todo as done
v 8.10.6 v 8.10.6
- Upgrade Rails to 4.2.7.1 for security fixes. !5781 - Upgrade Rails to 4.2.7.1 for security fixes. !5781
......
...@@ -53,7 +53,7 @@ gem 'browser', '~> 2.2' ...@@ -53,7 +53,7 @@ gem 'browser', '~> 2.2'
# Extracting information from a git repository # Extracting information from a git repository
# Provide access to Gitlab::Git library # Provide access to Gitlab::Git library
gem 'gitlab_git', '~> 10.4.5' gem 'gitlab_git', '~> 10.4.7'
# LDAP Auth # LDAP Auth
# GitLab fork with several improvements to original library. For full list of changes # GitLab fork with several improvements to original library. For full list of changes
...@@ -201,7 +201,7 @@ gem 'licensee', '~> 8.0.0' ...@@ -201,7 +201,7 @@ gem 'licensee', '~> 8.0.0'
gem 'rack-attack', '~> 4.3.1' gem 'rack-attack', '~> 4.3.1'
# Ace editor # Ace editor
gem 'ace-rails-ap', '~> 4.0.2' gem 'ace-rails-ap', '~> 4.1.0'
# Keyboard shortcuts # Keyboard shortcuts
gem 'mousetrap-rails', '~> 1.4.6' gem 'mousetrap-rails', '~> 1.4.6'
...@@ -209,7 +209,8 @@ gem 'mousetrap-rails', '~> 1.4.6' ...@@ -209,7 +209,8 @@ gem 'mousetrap-rails', '~> 1.4.6'
# Detect and convert string character encoding # Detect and convert string character encoding
gem 'charlock_holmes', '~> 0.7.3' gem 'charlock_holmes', '~> 0.7.3'
# Parse duration # Parse time & duration
gem 'chronic', '~> 0.10.2'
gem 'chronic_duration', '~> 0.10.6' gem 'chronic_duration', '~> 0.10.6'
gem 'sass-rails', '~> 5.0.0' gem 'sass-rails', '~> 5.0.0'
......
...@@ -2,7 +2,7 @@ GEM ...@@ -2,7 +2,7 @@ GEM
remote: https://rubygems.org/ remote: https://rubygems.org/
specs: specs:
RedCloth (4.3.2) RedCloth (4.3.2)
ace-rails-ap (4.0.2) ace-rails-ap (4.1.0)
actionmailer (4.2.7.1) actionmailer (4.2.7.1)
actionpack (= 4.2.7.1) actionpack (= 4.2.7.1)
actionview (= 4.2.7.1) actionview (= 4.2.7.1)
...@@ -128,6 +128,7 @@ GEM ...@@ -128,6 +128,7 @@ GEM
mime-types (>= 1.16) mime-types (>= 1.16)
cause (0.1) cause (0.1)
charlock_holmes (0.7.3) charlock_holmes (0.7.3)
chronic (0.10.2)
chronic_duration (0.10.6) chronic_duration (0.10.6)
numerizer (~> 0.1.1) numerizer (~> 0.1.1)
chunky_png (1.3.5) chunky_png (1.3.5)
...@@ -278,7 +279,7 @@ GEM ...@@ -278,7 +279,7 @@ GEM
diff-lcs (~> 1.1) diff-lcs (~> 1.1)
mime-types (>= 1.16, < 3) mime-types (>= 1.16, < 3)
posix-spawn (~> 0.3) posix-spawn (~> 0.3)
gitlab_git (10.4.5) gitlab_git (10.4.7)
activesupport (~> 4.0) activesupport (~> 4.0)
charlock_holmes (~> 0.7.3) charlock_holmes (~> 0.7.3)
github-linguist (~> 4.7.0) github-linguist (~> 4.7.0)
...@@ -798,7 +799,7 @@ PLATFORMS ...@@ -798,7 +799,7 @@ PLATFORMS
DEPENDENCIES DEPENDENCIES
RedCloth (~> 4.3.2) RedCloth (~> 4.3.2)
ace-rails-ap (~> 4.0.2) ace-rails-ap (~> 4.1.0)
activerecord-session_store (~> 1.0.0) activerecord-session_store (~> 1.0.0)
acts-as-taggable-on (~> 3.4) acts-as-taggable-on (~> 3.4)
addressable (~> 2.3.8) addressable (~> 2.3.8)
...@@ -824,6 +825,7 @@ DEPENDENCIES ...@@ -824,6 +825,7 @@ DEPENDENCIES
capybara-screenshot (~> 1.0.0) capybara-screenshot (~> 1.0.0)
carrierwave (~> 0.10.0) carrierwave (~> 0.10.0)
charlock_holmes (~> 0.7.3) charlock_holmes (~> 0.7.3)
chronic (~> 0.10.2)
chronic_duration (~> 0.10.6) chronic_duration (~> 0.10.6)
coffee-rails (~> 4.1.0) coffee-rails (~> 4.1.0)
connection_pool (~> 2.0) connection_pool (~> 2.0)
...@@ -857,7 +859,7 @@ DEPENDENCIES ...@@ -857,7 +859,7 @@ DEPENDENCIES
github-linguist (~> 4.7.0) github-linguist (~> 4.7.0)
github-markup (~> 1.4) github-markup (~> 1.4)
gitlab-flowdock-git-hook (~> 1.0.1) gitlab-flowdock-git-hook (~> 1.0.1)
gitlab_git (~> 10.4.5) gitlab_git (~> 10.4.7)
gitlab_meta (= 7.0) gitlab_meta (= 7.0)
gitlab_omniauth-ldap (~> 1.2.1) gitlab_omniauth-ldap (~> 1.2.1)
gollum-lib (~> 4.2) gollum-lib (~> 4.2)
......
window.gl = window.gl || {};
((global) => {
const MAX_MESSAGE_LENGTH = 500;
const MESSAGE_CELL_SELECTOR = '.abuse-reports .message';
class AbuseReports {
constructor() {
$(MESSAGE_CELL_SELECTOR).each(this.truncateLongMessage);
$(document)
.off('click', MESSAGE_CELL_SELECTOR)
.on('click', MESSAGE_CELL_SELECTOR, this.toggleMessageTruncation);
}
truncateLongMessage() {
const $messageCellElement = $(this);
const reportMessage = $messageCellElement.text();
if (reportMessage.length > MAX_MESSAGE_LENGTH) {
$messageCellElement.data('original-message', reportMessage);
$messageCellElement.data('message-truncated', 'true');
$messageCellElement.text(global.text.truncate(reportMessage, MAX_MESSAGE_LENGTH));
}
}
toggleMessageTruncation() {
const $messageCellElement = $(this);
const originalMessage = $messageCellElement.data('original-message');
if (!originalMessage) return;
if ($messageCellElement.data('message-truncated') === 'true') {
$messageCellElement.data('message-truncated', 'false');
$messageCellElement.text(originalMessage);
} else {
$messageCellElement.data('message-truncated', 'true');
$messageCellElement.text(`${originalMessage.substr(0, (MAX_MESSAGE_LENGTH - 3))}...`);
}
}
}
global.AbuseReports = AbuseReports;
})(window.gl);
...@@ -26,7 +26,7 @@ ...@@ -26,7 +26,7 @@
/*= require bootstrap/tooltip */ /*= require bootstrap/tooltip */
/*= require bootstrap/popover */ /*= require bootstrap/popover */
/*= require select2 */ /*= require select2 */
/*= require ace/ace */ /*= require ace-rails-ap */
/*= require ace/ext-searchbox */ /*= require ace/ext-searchbox */
/*= require underscore */ /*= require underscore */
/*= require dropzone */ /*= require dropzone */
......
(function() { (function() {
this.AwardsHandler = (function() { this.AwardsHandler = (function() {
const FROM_SENTENCE_REGEX = /(?:, and | and |, )/; //For separating lists produced by ruby's Array#toSentence
function AwardsHandler() { function AwardsHandler() {
this.aliases = gl.emojiAliases(); this.aliases = gl.emojiAliases();
$(document).off('click', '.js-add-award').on('click', '.js-add-award', (function(_this) { $(document).off('click', '.js-add-award').on('click', '.js-add-award', (function(_this) {
...@@ -130,7 +131,7 @@ ...@@ -130,7 +131,7 @@
counter = $emojiButton.find('.js-counter'); counter = $emojiButton.find('.js-counter');
counter.text(parseInt(counter.text()) + 1); counter.text(parseInt(counter.text()) + 1);
$emojiButton.addClass('active'); $emojiButton.addClass('active');
this.addMeToUserList(votesBlock, emoji); this.addYouToUserList(votesBlock, emoji);
return this.animateEmoji($emojiButton); return this.animateEmoji($emojiButton);
} }
} else { } else {
...@@ -176,11 +177,11 @@ ...@@ -176,11 +177,11 @@
counterNumber = parseInt(counter.text(), 10); counterNumber = parseInt(counter.text(), 10);
if (counterNumber > 1) { if (counterNumber > 1) {
counter.text(counterNumber - 1); counter.text(counterNumber - 1);
this.removeMeFromUserList($emojiButton, emoji); this.removeYouFromUserList($emojiButton, emoji);
} else if (emoji === 'thumbsup' || emoji === 'thumbsdown') { } else if (emoji === 'thumbsup' || emoji === 'thumbsdown') {
$emojiButton.tooltip('destroy'); $emojiButton.tooltip('destroy');
counter.text('0'); counter.text('0');
this.removeMeFromUserList($emojiButton, emoji); this.removeYouFromUserList($emojiButton, emoji);
if ($emojiButton.parents('.note').length) { if ($emojiButton.parents('.note').length) {
this.removeEmoji($emojiButton); this.removeEmoji($emojiButton);
} }
...@@ -204,43 +205,48 @@ ...@@ -204,43 +205,48 @@
return $awardBlock.attr('data-original-title') || $awardBlock.attr('data-title') || ''; return $awardBlock.attr('data-original-title') || $awardBlock.attr('data-title') || '';
}; };
AwardsHandler.prototype.removeMeFromUserList = function($emojiButton, emoji) { AwardsHandler.prototype.toSentence = function(list) {
if(list.length <= 2){
return list.join(' and ');
}
else{
return list.slice(0, -1).join(', ') + ', and ' + list[list.length - 1];
}
};
AwardsHandler.prototype.removeYouFromUserList = function($emojiButton, emoji) {
var authors, awardBlock, newAuthors, originalTitle; var authors, awardBlock, newAuthors, originalTitle;
awardBlock = $emojiButton; awardBlock = $emojiButton;
originalTitle = this.getAwardTooltip(awardBlock); originalTitle = this.getAwardTooltip(awardBlock);
authors = originalTitle.split(', '); authors = originalTitle.split(FROM_SENTENCE_REGEX);
authors.splice(authors.indexOf('me'), 1); authors.splice(authors.indexOf('You'), 1);
newAuthors = authors.join(', '); return awardBlock
awardBlock.closest('.js-emoji-btn').removeData('original-title').attr('data-original-title', newAuthors); .closest('.js-emoji-btn')
return this.resetTooltip(awardBlock); .removeData('title')
}; .removeAttr('data-title')
.removeAttr('data-original-title')
AwardsHandler.prototype.addMeToUserList = function(votesBlock, emoji) { .attr('title', this.toSentence(authors))
.tooltip('fixTitle');
};
AwardsHandler.prototype.addYouToUserList = function(votesBlock, emoji) {
var awardBlock, origTitle, users; var awardBlock, origTitle, users;
awardBlock = this.findEmojiIcon(votesBlock, emoji).parent(); awardBlock = this.findEmojiIcon(votesBlock, emoji).parent();
origTitle = this.getAwardTooltip(awardBlock); origTitle = this.getAwardTooltip(awardBlock);
users = []; users = [];
if (origTitle) { if (origTitle) {
users = origTitle.trim().split(', '); users = origTitle.trim().split(FROM_SENTENCE_REGEX);
} }
users.push('me'); users.unshift('You');
awardBlock.attr('title', users.join(', ')); return awardBlock
return this.resetTooltip(awardBlock); .attr('title', this.toSentence(users))
}; .tooltip('fixTitle');
AwardsHandler.prototype.resetTooltip = function(award) {
var cb;
award.tooltip('destroy');
cb = function() {
return award.tooltip();
};
return setTimeout(cb, 200);
}; };
AwardsHandler.prototype.createEmoji_ = function(votesBlock, emoji) { AwardsHandler.prototype.createEmoji_ = function(votesBlock, emoji) {
var $emojiButton, buttonHtml, emojiCssClass; var $emojiButton, buttonHtml, emojiCssClass;
emojiCssClass = this.resolveNameToCssClass(emoji); emojiCssClass = this.resolveNameToCssClass(emoji);
buttonHtml = "<button class='btn award-control js-emoji-btn has-tooltip active' title='me' data-placement='bottom'> <div class='icon emoji-icon " + emojiCssClass + "' data-emoji='" + emoji + "'></div> <span class='award-control-text js-counter'>1</span> </button>"; buttonHtml = "<button class='btn award-control js-emoji-btn has-tooltip active' title='You' data-placement='bottom'> <div class='icon emoji-icon " + emojiCssClass + "' data-emoji='" + emoji + "'></div> <span class='award-control-text js-counter'>1</span> </button>";
$emojiButton = $(buttonHtml); $emojiButton = $(buttonHtml);
$emojiButton.insertBefore(votesBlock.find('.js-award-holder')).find('.emoji-icon').data('emoji', emoji); $emojiButton.insertBefore(votesBlock.find('.js-award-holder')).find('.emoji-icon').data('emoji', emoji);
this.animateEmoji($emojiButton); this.animateEmoji($emojiButton);
......
Vue.http.interceptors.push((request, next) => { Vue.http.interceptors.push((request, next) => {
Vue.activeResources = Vue.activeResources ? Vue.activeResources + 1 : 1; Vue.activeResources = Vue.activeResources ? Vue.activeResources + 1 : 1;
Vue.nextTick(() => {
setTimeout(() => { setTimeout(() => {
Vue.activeResources--; Vue.activeResources--;
}, 500); }, 500);
});
next(); next();
}); });
...@@ -199,6 +199,9 @@ ...@@ -199,6 +199,9 @@
case 'edit': case 'edit':
new Labels(); new Labels();
} }
case 'abuse_reports':
new gl.AbuseReports();
break;
} }
break; break;
case 'dashboard': case 'dashboard':
......
...@@ -223,7 +223,7 @@ ...@@ -223,7 +223,7 @@
} }
} }
}); });
return this.input.atwho({ this.input.atwho({
at: '~', at: '~',
alias: 'labels', alias: 'labels',
searchKey: 'search', searchKey: 'search',
...@@ -249,6 +249,68 @@ ...@@ -249,6 +249,68 @@
} }
} }
}); });
// We don't instantiate the slash commands autocomplete for note and issue/MR edit forms
this.input.filter('[data-supports-slash-commands="true"]').atwho({
at: '/',
alias: 'commands',
searchKey: 'search',
displayTpl: function(value) {
var tpl = '<li>/${name}';
if (value.aliases.length > 0) {
tpl += ' <small>(or /<%- aliases.join(", /") %>)</small>';
}
if (value.params.length > 0) {
tpl += ' <small><%- params.join(" ") %></small>';
}
if (value.description !== '') {
tpl += '<small class="description"><i><%- description %></i></small>';
}
tpl += '</li>';
return _.template(tpl)(value);
},
insertTpl: function(value) {
var tpl = "/${name} ";
var reference_prefix = null;
if (value.params.length > 0) {
reference_prefix = value.params[0][0];
if (/^[@%~]/.test(reference_prefix)) {
tpl += '<%- reference_prefix %>';
}
}
return _.template(tpl)({ reference_prefix: reference_prefix });
},
suffix: '',
callbacks: {
sorter: this.DefaultOptions.sorter,
filter: this.DefaultOptions.filter,
beforeInsert: this.DefaultOptions.beforeInsert,
beforeSave: function(commands) {
return $.map(commands, function(c) {
var search = c.name;
if (c.aliases.length > 0) {
search = search + " " + c.aliases.join(" ");
}
return {
name: c.name,
aliases: c.aliases,
params: c.params,
description: c.description,
search: search
};
});
},
matcher: function(flag, subtext, should_startWithSpace, acceptSpaceBar) {
var regexp = /(?:^|\n)\/([A-Za-z_]*)$/gi
var match = regexp.exec(subtext);
if (match) {
return match[1];
} else {
return null;
}
}
}
});
return;
}, },
destroyAtWho: function() { destroyAtWho: function() {
return this.input.atwho('destroy'); return this.input.atwho('destroy');
...@@ -265,6 +327,7 @@ ...@@ -265,6 +327,7 @@
this.input.atwho('load', 'mergerequests', data.mergerequests); this.input.atwho('load', 'mergerequests', data.mergerequests);
this.input.atwho('load', ':', data.emojis); this.input.atwho('load', ':', data.emojis);
this.input.atwho('load', '~', data.labels); this.input.atwho('load', '~', data.labels);
this.input.atwho('load', '/', data.commands);
return $(':focus').trigger('keyup'); return $(':focus').trigger('keyup');
} }
}; };
......
...@@ -104,9 +104,12 @@ ...@@ -104,9 +104,12 @@
return self.updateText($this.closest('.md-area').find('textarea'), $this.data('md-tag'), $this.data('md-block'), !$this.data('md-prepend')); return self.updateText($this.closest('.md-area').find('textarea'), $this.data('md-tag'), $this.data('md-block'), !$this.data('md-prepend'));
}); });
}; };
return gl.text.removeListeners = function(form) { gl.text.removeListeners = function(form) {
return $('.js-md', form).off(); return $('.js-md', form).off();
}; };
return gl.text.truncate = function(string, maxLength) {
return string.substr(0, (maxLength - 3)) + '...';
};
})(window); })(window);
}).call(this); }).call(this);
...@@ -15,6 +15,7 @@ ...@@ -15,6 +15,7 @@
function MergeRequestTabs(opts) { function MergeRequestTabs(opts) {
this.opts = opts != null ? opts : {}; this.opts = opts != null ? opts : {};
this.opts.setUrl = this.opts.setUrl !== undefined ? this.opts.setUrl : true;
this.setCurrentAction = bind(this.setCurrentAction, this); this.setCurrentAction = bind(this.setCurrentAction, this);
this.tabShown = bind(this.tabShown, this); this.tabShown = bind(this.tabShown, this);
this.showTab = bind(this.showTab, this); this.showTab = bind(this.showTab, this);
...@@ -58,7 +59,9 @@ ...@@ -58,7 +59,9 @@
} else { } else {
this.expandView(); this.expandView();
} }
return this.setCurrentAction(action); if (this.opts.setUrl) {
this.setCurrentAction(action);
}
}; };
MergeRequestTabs.prototype.scrollToElement = function(container) { MergeRequestTabs.prototype.scrollToElement = function(container) {
......
...@@ -231,7 +231,13 @@ ...@@ -231,7 +231,13 @@
var $notesList, votesBlock; var $notesList, votesBlock;
if (!note.valid) { if (!note.valid) {
if (note.award) { if (note.award) {
new Flash('You have already awarded this emoji!', 'alert'); new Flash('You have already awarded this emoji!', 'alert', this.parentTimeline);
}
else {
if (note.errors.commands_only) {
new Flash(note.errors.commands_only, 'notice', this.parentTimeline);
this.refresh();
}
} }
return; return;
} }
...@@ -245,6 +251,7 @@ ...@@ -245,6 +251,7 @@
$notesList.append(note.html).syntaxHighlight(); $notesList.append(note.html).syntaxHighlight();
gl.utils.localTimeAgo($notesList.find("#note_" + note.id + " .js-timeago"), false); gl.utils.localTimeAgo($notesList.find("#note_" + note.id + " .js-timeago"), false);
this.initTaskList(); this.initTaskList();
this.refresh();
return this.updateNotesCount(1); return this.updateNotesCount(1);
} }
}; };
......
...@@ -147,3 +147,8 @@ ...@@ -147,3 +147,8 @@
color: $gl-link-color; color: $gl-link-color;
} }
} }
.atwho-view small.description {
float: right;
padding: 3px 5px;
}
...@@ -45,7 +45,6 @@ ...@@ -45,7 +45,6 @@
.line_content { .line_content {
padding-left: 0.5em; padding-left: 0.5em;
padding-right: 0.5em; padding-right: 0.5em;
white-space: pre;
&.old { &.old {
background-color: $line-removed; background-color: $line-removed;
...@@ -71,6 +70,10 @@ ...@@ -71,6 +70,10 @@
} }
} }
pre {
margin: 0;
}
span.highlight_word { span.highlight_word {
background-color: #fafe3d !important; background-color: #fafe3d !important;
} }
......
...@@ -72,7 +72,6 @@ ...@@ -72,7 +72,6 @@
margin-bottom: 20px; margin-bottom: 20px;
} }
// Users List // Users List
.users-list { .users-list {
...@@ -98,3 +97,44 @@ ...@@ -98,3 +97,44 @@
} }
} }
} }
.abuse-reports {
.table {
table-layout: fixed;
}
.subheading {
padding-bottom: $gl-padding;
}
.message {
word-wrap: break-word;
}
.btn {
white-space: normal;
padding: $gl-btn-padding;
}
th {
width: 15%;
&.wide {
width: 55%;
}
}
@media (max-width: $screen-sm-max) {
th {
width: 100%;
}
td {
width: 100%;
float: left;
}
}
.no-reports {
.emoji-icon {
margin-left: $btn-side-margin;
margin-top: 3px;
}
span {
font-size: 19px;
}
}
}
...@@ -228,3 +228,9 @@ ...@@ -228,3 +228,9 @@
} }
} }
} }
table.u2f-registrations {
th:not(:last-child), td:not(:last-child) {
border-right: solid 1px transparent;
}
}
\ No newline at end of file
...@@ -6,7 +6,7 @@ class Dashboard::TodosController < Dashboard::ApplicationController ...@@ -6,7 +6,7 @@ class Dashboard::TodosController < Dashboard::ApplicationController
end end
def destroy def destroy
TodoService.new.mark_todos_as_done([todo], current_user) TodoService.new.mark_todos_as_done_by_ids([params[:id]], current_user)
respond_to do |format| respond_to do |format|
format.html { redirect_to dashboard_todos_path, notice: 'Todo was successfully marked as done.' } format.html { redirect_to dashboard_todos_path, notice: 'Todo was successfully marked as done.' }
...@@ -27,10 +27,6 @@ class Dashboard::TodosController < Dashboard::ApplicationController ...@@ -27,10 +27,6 @@ class Dashboard::TodosController < Dashboard::ApplicationController
private private
def todo
@todo ||= find_todos.find(params[:id])
end
def find_todos def find_todos
@todos ||= TodosFinder.new(current_user, params).execute @todos ||= TodosFinder.new(current_user, params).execute
end end
......
...@@ -43,11 +43,11 @@ class Profiles::TwoFactorAuthsController < Profiles::ApplicationController ...@@ -43,11 +43,11 @@ class Profiles::TwoFactorAuthsController < Profiles::ApplicationController
# A U2F (universal 2nd factor) device's information is stored after successful # A U2F (universal 2nd factor) device's information is stored after successful
# registration, which is then used while 2FA authentication is taking place. # registration, which is then used while 2FA authentication is taking place.
def create_u2f def create_u2f
@u2f_registration = U2fRegistration.register(current_user, u2f_app_id, params[:device_response], session[:challenges]) @u2f_registration = U2fRegistration.register(current_user, u2f_app_id, u2f_registration_params, session[:challenges])
if @u2f_registration.persisted? if @u2f_registration.persisted?
session.delete(:challenges) session.delete(:challenges)
redirect_to profile_account_path, notice: "Your U2F device was registered!" redirect_to profile_two_factor_auth_path, notice: "Your U2F device was registered!"
else else
@qr_code = build_qr_code @qr_code = build_qr_code
setup_u2f_registration setup_u2f_registration
...@@ -91,15 +91,19 @@ class Profiles::TwoFactorAuthsController < Profiles::ApplicationController ...@@ -91,15 +91,19 @@ class Profiles::TwoFactorAuthsController < Profiles::ApplicationController
# Actual communication is performed using a Javascript API # Actual communication is performed using a Javascript API
def setup_u2f_registration def setup_u2f_registration
@u2f_registration ||= U2fRegistration.new @u2f_registration ||= U2fRegistration.new
@registration_key_handles = current_user.u2f_registrations.pluck(:key_handle) @u2f_registrations = current_user.u2f_registrations
u2f = U2F::U2F.new(u2f_app_id) u2f = U2F::U2F.new(u2f_app_id)
registration_requests = u2f.registration_requests registration_requests = u2f.registration_requests
sign_requests = u2f.authentication_requests(@registration_key_handles) sign_requests = u2f.authentication_requests(@u2f_registrations.map(&:key_handle))
session[:challenges] = registration_requests.map(&:challenge) session[:challenges] = registration_requests.map(&:challenge)
gon.push(u2f: { challenges: session[:challenges], app_id: u2f_app_id, gon.push(u2f: { challenges: session[:challenges], app_id: u2f_app_id,
register_requests: registration_requests, register_requests: registration_requests,
sign_requests: sign_requests }) sign_requests: sign_requests })
end end
def u2f_registration_params
params.require(:u2f_registration).permit(:device_response, :name)
end
end end
class Profiles::U2fRegistrationsController < Profiles::ApplicationController
def destroy
u2f_registration = current_user.u2f_registrations.find(params[:id])
u2f_registration.destroy
redirect_to profile_two_factor_auth_path, notice: "Successfully deleted U2F device."
end
end
...@@ -83,6 +83,7 @@ class Projects::ApplicationController < ApplicationController ...@@ -83,6 +83,7 @@ class Projects::ApplicationController < ApplicationController
end end
def apply_diff_view_cookie! def apply_diff_view_cookie!
@show_changes_tab = params[:view].present?
cookies.permanent[:diff_view] = params.delete(:view) if params[:view].present? cookies.permanent[:diff_view] = params.delete(:view) if params[:view].present?
end end
......
...@@ -93,7 +93,7 @@ class Projects::CommitController < Projects::ApplicationController ...@@ -93,7 +93,7 @@ class Projects::CommitController < Projects::ApplicationController
end end
def commit def commit
@commit ||= @project.commit(params[:id]) @noteable = @commit ||= @project.commit(params[:id])
end end
def pipelines def pipelines
......
...@@ -177,11 +177,7 @@ class Projects::IssuesController < Projects::ApplicationController ...@@ -177,11 +177,7 @@ class Projects::IssuesController < Projects::ApplicationController
protected protected
def issue def issue
@issue ||= begin @noteable = @issue ||= @project.issues.find_by(iid: params[:id]) || redirect_old
@project.issues.find_by!(iid: params[:id])
rescue ActiveRecord::RecordNotFound
redirect_old
end
end end
alias_method :subscribable_resource, :issue alias_method :subscribable_resource, :issue
alias_method :issuable, :issue alias_method :issuable, :issue
...@@ -226,7 +222,6 @@ class Projects::IssuesController < Projects::ApplicationController ...@@ -226,7 +222,6 @@ class Projects::IssuesController < Projects::ApplicationController
if issue if issue
redirect_to issue_path(issue) redirect_to issue_path(issue)
return
else else
raise ActiveRecord::RecordNotFound.new raise ActiveRecord::RecordNotFound.new
end end
......
...@@ -216,7 +216,6 @@ class Projects::MergeRequestsController < Projects::ApplicationController ...@@ -216,7 +216,6 @@ class Projects::MergeRequestsController < Projects::ApplicationController
@base_commit = @merge_request.diff_base_commit @base_commit = @merge_request.diff_base_commit
@diffs = @merge_request.diffs(diff_options) if @merge_request.compare @diffs = @merge_request.diffs(diff_options) if @merge_request.compare
@diff_notes_disabled = true @diff_notes_disabled = true
@pipeline = @merge_request.pipeline @pipeline = @merge_request.pipeline
@statuses = @pipeline.statuses.relevant if @pipeline @statuses = @pipeline.statuses.relevant if @pipeline
...@@ -382,7 +381,7 @@ class Projects::MergeRequestsController < Projects::ApplicationController ...@@ -382,7 +381,7 @@ class Projects::MergeRequestsController < Projects::ApplicationController
end end
def merge_request def merge_request
@merge_request ||= @project.merge_requests.find_by!(iid: params[:id]) @issuable = @merge_request ||= @project.merge_requests.find_by!(iid: params[:id])
end end
alias_method :subscribable_resource, :merge_request alias_method :subscribable_resource, :merge_request
alias_method :issuable, :merge_request alias_method :issuable, :merge_request
......
...@@ -125,7 +125,7 @@ class Projects::NotesController < Projects::ApplicationController ...@@ -125,7 +125,7 @@ class Projects::NotesController < Projects::ApplicationController
id: note.id, id: note.id,
name: note.name name: note.name
} }
elsif note.valid? elsif note.persisted?
Banzai::NoteRenderer.render([note], @project, current_user) Banzai::NoteRenderer.render([note], @project, current_user)
attrs = { attrs = {
......
...@@ -134,10 +134,22 @@ class ProjectsController < Projects::ApplicationController ...@@ -134,10 +134,22 @@ class ProjectsController < Projects::ApplicationController
end end
def autocomplete_sources def autocomplete_sources
note_type = params['type'] noteable =
note_id = params['type_id'] case params[:type]
when 'Issue'
IssuesFinder.new(current_user, project_id: @project.id, state: 'all').
execute.find_by(iid: params[:type_id])
when 'MergeRequest'
MergeRequestsFinder.new(current_user, project_id: @project.id, state: 'all').
execute.find_by(iid: params[:type_id])
when 'Commit'
@project.commit(params[:type_id])
else
nil
end
autocomplete = ::Projects::AutocompleteService.new(@project, current_user) autocomplete = ::Projects::AutocompleteService.new(@project, current_user)
participants = ::Projects::ParticipantsService.new(@project, current_user).execute(note_type, note_id) participants = ::Projects::ParticipantsService.new(@project, current_user).execute(noteable)
@suggestions = { @suggestions = {
emojis: Gitlab::AwardEmoji.urls, emojis: Gitlab::AwardEmoji.urls,
...@@ -145,7 +157,8 @@ class ProjectsController < Projects::ApplicationController ...@@ -145,7 +157,8 @@ class ProjectsController < Projects::ApplicationController
milestones: autocomplete.milestones, milestones: autocomplete.milestones,
mergerequests: autocomplete.merge_requests, mergerequests: autocomplete.merge_requests,
labels: autocomplete.labels, labels: autocomplete.labels,
members: participants members: participants,
commands: autocomplete.commands(noteable, params[:type])
} }
respond_to do |format| respond_to do |format|
......
...@@ -17,7 +17,7 @@ class TodosFinder ...@@ -17,7 +17,7 @@ class TodosFinder
attr_accessor :current_user, :params attr_accessor :current_user, :params
def initialize(current_user, params) def initialize(current_user, params = {})
@current_user = current_user @current_user = current_user
@params = params @params = params
end end
......
...@@ -11,17 +11,14 @@ module BlobHelper ...@@ -11,17 +11,14 @@ module BlobHelper
def edit_blob_link(project = @project, ref = @ref, path = @path, options = {}) def edit_blob_link(project = @project, ref = @ref, path = @path, options = {})
return unless current_user return unless current_user
blob = project.repository.blob_at(ref, path) rescue nil blob = options.delete(:blob)
blob ||= project.repository.blob_at(ref, path) rescue nil
return unless blob return unless blob
from_mr = options[:from_merge_request_id]
link_opts = {}
link_opts[:from_merge_request_id] = from_mr if from_mr
edit_path = namespace_project_edit_blob_path(project.namespace, project, edit_path = namespace_project_edit_blob_path(project.namespace, project,
tree_join(ref, path), tree_join(ref, path),
link_opts) options[:link_opts])
if !on_top_of_branch?(project, ref) if !on_top_of_branch?(project, ref)
button_tag "Edit", class: "btn disabled has-tooltip btn-file-option", title: "You can only edit files when you are on a branch", data: { container: 'body' } button_tag "Edit", class: "btn disabled has-tooltip btn-file-option", title: "You can only edit files when you are on a branch", data: { container: 'body' }
......
...@@ -114,9 +114,17 @@ module IssuesHelper ...@@ -114,9 +114,17 @@ module IssuesHelper
end end
def award_user_list(awards, current_user) def award_user_list(awards, current_user)
awards.map do |award| names = awards.map do |award|
award.user == current_user ? 'me' : award.user.name award.user == current_user ? 'You' : award.user.name
end.join(', ') end
# Take first 9 OR current user + first 9
current_user_name = names.delete('You')
names = names.first(9).insert(0, current_user_name).compact
names << "#{awards.size - names.size} more." if awards.size > names.size
names.to_sentence
end end
def award_active_class(awards, current_user) def award_active_class(awards, current_user)
......
...@@ -3,18 +3,19 @@ ...@@ -3,18 +3,19 @@
class U2fRegistration < ActiveRecord::Base class U2fRegistration < ActiveRecord::Base
belongs_to :user belongs_to :user
def self.register(user, app_id, json_response, challenges) def self.register(user, app_id, params, challenges)
u2f = U2F::U2F.new(app_id) u2f = U2F::U2F.new(app_id)
registration = self.new registration = self.new
begin begin
response = U2F::RegisterResponse.load_from_json(json_response) response = U2F::RegisterResponse.load_from_json(params[:device_response])
registration_data = u2f.register!(challenges, response) registration_data = u2f.register!(challenges, response)
registration.update(certificate: registration_data.certificate, registration.update(certificate: registration_data.certificate,
key_handle: registration_data.key_handle, key_handle: registration_data.key_handle,
public_key: registration_data.public_key, public_key: registration_data.public_key,
counter: registration_data.counter, counter: registration_data.counter,
user: user) user: user,
name: params[:name])
rescue JSON::ParserError, NoMethodError, ArgumentError rescue JSON::ParserError, NoMethodError, ArgumentError
registration.errors.add(:base, 'Your U2F device did not send a valid JSON response.') registration.errors.add(:base, 'Your U2F device did not send a valid JSON response.')
rescue U2F::Error => e rescue U2F::Error => e
......
...@@ -69,15 +69,10 @@ class IssuableBaseService < BaseService ...@@ -69,15 +69,10 @@ class IssuableBaseService < BaseService
end end
def filter_labels def filter_labels
if params[:add_label_ids].present? || params[:remove_label_ids].present?
params.delete(:label_ids)
filter_labels_in_param(:add_label_ids) filter_labels_in_param(:add_label_ids)
filter_labels_in_param(:remove_label_ids) filter_labels_in_param(:remove_label_ids)
else
filter_labels_in_param(:label_ids) filter_labels_in_param(:label_ids)
end end
end
def filter_labels_in_param(key) def filter_labels_in_param(key)
return if params[key].to_a.empty? return if params[key].to_a.empty?
...@@ -85,27 +80,86 @@ class IssuableBaseService < BaseService ...@@ -85,27 +80,86 @@ class IssuableBaseService < BaseService
params[key] = project.labels.where(id: params[key]).pluck(:id) params[key] = project.labels.where(id: params[key]).pluck(:id)
end end
def update_issuable(issuable, attributes) def process_label_ids(attributes, existing_label_ids: nil)
issuable.with_transaction_returning_status do label_ids = attributes.delete(:label_ids)
add_label_ids = attributes.delete(:add_label_ids) add_label_ids = attributes.delete(:add_label_ids)
remove_label_ids = attributes.delete(:remove_label_ids) remove_label_ids = attributes.delete(:remove_label_ids)
issuable.label_ids |= add_label_ids if add_label_ids new_label_ids = existing_label_ids || label_ids || []
issuable.label_ids -= remove_label_ids if remove_label_ids
if add_label_ids.blank? && remove_label_ids.blank?
new_label_ids = label_ids if label_ids
else
new_label_ids |= add_label_ids if add_label_ids
new_label_ids -= remove_label_ids if remove_label_ids
end
new_label_ids
end
def merge_slash_commands_into_params!(issuable)
description, command_params =
SlashCommands::InterpretService.new(project, current_user).
execute(params[:description], issuable)
issuable.assign_attributes(attributes.merge(updated_by: current_user)) params[:description] = description
issuable.save params.merge!(command_params)
end
def create_issuable(issuable, attributes, label_ids:)
issuable.with_transaction_returning_status do
if issuable.save
issuable.update_attributes(label_ids: label_ids)
end
end
end
def create(issuable)
merge_slash_commands_into_params!(issuable)
filter_params
params.delete(:state_event)
params[:author] ||= current_user
label_ids = process_label_ids(params)
issuable.assign_attributes(params)
before_create(issuable)
if params.present? && create_issuable(issuable, params, label_ids: label_ids)
after_create(issuable)
issuable.create_cross_references!(current_user)
execute_hooks(issuable)
end
issuable
end
def before_create(issuable)
# To be overridden by subclasses
end
def after_create(issuable)
# To be overridden by subclasses
end
def update_issuable(issuable, attributes)
issuable.with_transaction_returning_status do
issuable.update(attributes.merge(updated_by: current_user))
end end
end end
def update(issuable) def update(issuable)
change_state(issuable) change_state(issuable)
change_subscription(issuable) change_subscription(issuable)
change_todo(issuable)
filter_params filter_params
old_labels = issuable.labels.to_a old_labels = issuable.labels.to_a
old_mentioned_users = issuable.mentioned_users.to_a old_mentioned_users = issuable.mentioned_users.to_a
params[:label_ids] = process_label_ids(params, existing_label_ids: issuable.label_ids)
if params.present? && update_issuable(issuable, params) if params.present? && update_issuable(issuable, params)
issuable.reset_events_cache issuable.reset_events_cache
handle_common_system_notes(issuable, old_labels: old_labels) handle_common_system_notes(issuable, old_labels: old_labels)
...@@ -135,6 +189,16 @@ class IssuableBaseService < BaseService ...@@ -135,6 +189,16 @@ class IssuableBaseService < BaseService
end end
end end
def change_todo(issuable)
case params.delete(:todo_event)
when 'add'
todo_service.mark_todo(issuable, current_user)
when 'done'
todo = TodosFinder.new(current_user).execute.find_by(target: issuable)
todo_service.mark_todos_as_done([todo], current_user) if todo
end
end
def has_changes?(issuable, old_labels: []) def has_changes?(issuable, old_labels: [])
valid_attrs = [:title, :description, :assignee_id, :milestone_id, :target_branch] valid_attrs = [:title, :description, :assignee_id, :milestone_id, :target_branch]
......
module Issues module Issues
class CloseService < Issues::BaseService class CloseService < Issues::BaseService
def execute(issue, commit: nil, notifications: true, system_note: true) def execute(issue, commit: nil, notifications: true, system_note: true)
return issue unless can?(current_user, :update_issue, issue)
if project.jira_tracker? && project.jira_service.active if project.jira_tracker? && project.jira_service.active
project.jira_service.execute(commit, issue) project.jira_service.execute(commit, issue)
todo_service.close_issue(issue, current_user) todo_service.close_issue(issue, current_user)
......
module Issues module Issues
class CreateService < Issues::BaseService class CreateService < Issues::BaseService
def execute def execute
filter_params
label_params = params.delete(:label_ids)
@request = params.delete(:request) @request = params.delete(:request)
@api = params.delete(:api) @api = params.delete(:api)
@issue = project.issues.new(params)
@issue.author = params[:author] || current_user
@issue.spam = spam_service.check(@api) @issue = project.issues.new
if @issue.save create(@issue)
@issue.update_attributes(label_ids: label_params) end
notification_service.new_issue(@issue, current_user)
todo_service.new_issue(@issue, current_user) def before_create(issuable)
event_service.open_issue(@issue, current_user) issuable.spam = spam_service.check(@api)
user_agent_detail_service.create
@issue.create_cross_references!(current_user)
execute_hooks(@issue, 'open')
end end
@issue def after_create(issuable)
event_service.open_issue(issuable, current_user)
notification_service.new_issue(issuable, current_user)
todo_service.new_issue(issuable, current_user)
user_agent_detail_service.create
end end
private private
......
module Issues module Issues
class ReopenService < Issues::BaseService class ReopenService < Issues::BaseService
def execute(issue) def execute(issue)
return issue unless can?(current_user, :update_issue, issue)
if issue.reopen if issue.reopen
event_service.reopen_issue(issue, current_user) event_service.reopen_issue(issue, current_user)
create_note(issue) create_note(issue)
......
module MergeRequests module MergeRequests
class CloseService < MergeRequests::BaseService class CloseService < MergeRequests::BaseService
def execute(merge_request, commit = nil) def execute(merge_request, commit = nil)
return merge_request unless can?(current_user, :update_merge_request, merge_request)
# If we close MergeRequest we want to ignore validation # If we close MergeRequest we want to ignore validation
# so we can close broken one (Ex. fork project removed) # so we can close broken one (Ex. fork project removed)
merge_request.allow_broken = true merge_request.allow_broken = true
......
...@@ -7,26 +7,19 @@ module MergeRequests ...@@ -7,26 +7,19 @@ module MergeRequests
source_project = @project source_project = @project
@project = Project.find(params[:target_project_id]) if params[:target_project_id] @project = Project.find(params[:target_project_id]) if params[:target_project_id]
filter_params params[:target_project_id] ||= source_project.id
label_params = params.delete(:label_ids)
force_remove_source_branch = params.delete(:force_remove_source_branch)
merge_request = MergeRequest.new(params) merge_request = MergeRequest.new
merge_request.source_project = source_project merge_request.source_project = source_project
merge_request.target_project ||= source_project merge_request.merge_params['force_remove_source_branch'] = params.delete(:force_remove_source_branch)
merge_request.author = current_user
merge_request.merge_params['force_remove_source_branch'] = force_remove_source_branch
if merge_request.save create(merge_request)
merge_request.update_attributes(label_ids: label_params)
event_service.open_mr(merge_request, current_user)
notification_service.new_merge_request(merge_request, current_user)
todo_service.new_merge_request(merge_request, current_user)
merge_request.create_cross_references!(current_user)
execute_hooks(merge_request)
end end
merge_request def after_create(issuable)
event_service.open_mr(issuable, current_user)
notification_service.new_merge_request(issuable, current_user)
todo_service.new_merge_request(issuable, current_user)
end end
end end
end end
module MergeRequests module MergeRequests
class ReopenService < MergeRequests::BaseService class ReopenService < MergeRequests::BaseService
def execute(merge_request) def execute(merge_request)
return merge_request unless can?(current_user, :update_merge_request, merge_request)
if merge_request.reopen if merge_request.reopen
event_service.reopen_mr(merge_request, current_user) event_service.reopen_mr(merge_request, current_user)
create_note(merge_request) create_note(merge_request)
......
...@@ -11,10 +11,33 @@ module Notes ...@@ -11,10 +11,33 @@ module Notes
return noteable.create_award_emoji(note.award_emoji_name, current_user) return noteable.create_award_emoji(note.award_emoji_name, current_user)
end end
if note.save # We execute commands (extracted from `params[:note]`) on the noteable
# **before** we save the note because if the note consists of commands
# only, there is no need be create a note!
slash_commands_service = SlashCommandsService.new(project, current_user)
if slash_commands_service.supported?(note)
content, command_params = slash_commands_service.extract_commands(note)
only_commands = content.empty?
note.note = content
end
if !only_commands && note.save
# Finish the harder work in the background # Finish the harder work in the background
NewNoteWorker.perform_in(2.seconds, note.id, params) NewNoteWorker.perform_in(2.seconds, note.id, params)
TodoService.new.new_note(note, current_user) todo_service.new_note(note, current_user)
end
if command_params && command_params.any?
slash_commands_service.execute(command_params, note)
# We must add the error after we call #save because errors are reset
# when #save is called
if only_commands
note.errors.add(:commands_only, 'Your commands have been executed!')
end
end end
note note
......
module Notes
class SlashCommandsService < BaseService
UPDATE_SERVICES = {
'Issue' => Issues::UpdateService,
'MergeRequest' => MergeRequests::UpdateService
}
def supported?(note)
noteable_update_service(note) &&
can?(current_user, :"update_#{note.noteable_type.underscore}", note.noteable)
end
def extract_commands(note)
return [note.note, {}] unless supported?(note)
SlashCommands::InterpretService.new(project, current_user).
execute(note.note, note.noteable)
end
def execute(command_params, note)
return if command_params.empty?
return unless supported?(note)
noteable_update_service(note).new(project, current_user, command_params).execute(note.noteable)
end
private
def noteable_update_service(note)
UPDATE_SERVICES[note.noteable_type]
end
end
end
module Projects module Projects
class AutocompleteService < BaseService class AutocompleteService < BaseService
def issues def issues
@project.issues.visible_to_user(current_user).opened.select([:iid, :title]) IssuesFinder.new(current_user, project_id: project.id, state: 'opened').execute.select([:iid, :title])
end end
def milestones def milestones
...@@ -9,11 +9,34 @@ module Projects ...@@ -9,11 +9,34 @@ module Projects
end end
def merge_requests def merge_requests
@project.merge_requests.opened.select([:iid, :title]) MergeRequestsFinder.new(current_user, project_id: project.id, state: 'opened').execute.select([:iid, :title])
end end
def labels def labels
@project.labels.select([:title, :color]) @project.labels.select([:title, :color])
end end
def commands(noteable, type)
noteable ||=
case type
when 'Issue'
@project.issues.build
when 'MergeRequest'
@project.merge_requests.build
end
return [] unless noteable && noteable.is_a?(Issuable)
opts = {
project: project,
issuable: noteable,
current_user: current_user
}
SlashCommands::InterpretService.command_definitions.map do |definition|
next unless definition.available?(opts)
definition.to_h(opts)
end.compact
end
end end
end end
module Projects module Projects
class ParticipantsService < BaseService class ParticipantsService < BaseService
def execute(noteable_type, noteable_id) attr_reader :noteable
@noteable_type = noteable_type
@noteable_id = noteable_id def execute(noteable)
@noteable = noteable
project_members = sorted(project.team.members) project_members = sorted(project.team.members)
participants = target_owner + participants_in_target + all_members + groups + project_members participants = noteable_owner + participants_in_noteable + all_members + groups + project_members
participants.uniq participants.uniq
end end
def target def noteable_owner
@target ||= return [] unless noteable && noteable.author.present?
case @noteable_type
when "Issue"
project.issues.find_by_iid(@noteable_id)
when "MergeRequest"
project.merge_requests.find_by_iid(@noteable_id)
when "Commit"
project.commit(@noteable_id)
else
nil
end
end
def target_owner
return [] unless target && target.author.present?
[{ [{
name: target.author.name, name: noteable.author.name,
username: target.author.username username: noteable.author.username
}] }]
end end
def participants_in_target def participants_in_noteable
return [] unless target return [] unless noteable
users = target.participants(current_user) users = noteable.participants(current_user)
sorted(users) sorted(users)
end end
......
module SlashCommands
class InterpretService < BaseService
include Gitlab::SlashCommands::Dsl
attr_reader :issuable
# Takes a text and interprets the commands that are extracted from it.
# Returns the content without commands, and hash of changes to be applied to a record.
def execute(content, issuable)
@issuable = issuable
@updates = {}
opts = {
issuable: issuable,
current_user: current_user,
project: project
}
content, commands = extractor.extract_commands(content, opts)
commands.each do |name, arg|
definition = self.class.command_definitions_by_name[name.to_sym]
next unless definition
definition.execute(self, opts, arg)
end
[content, @updates]
end
private
def extractor
Gitlab::SlashCommands::Extractor.new(self.class.command_definitions)
end
desc do
"Close this #{issuable.to_ability_name.humanize(capitalize: false)}"
end
condition do
issuable.persisted? &&
issuable.open? &&
current_user.can?(:"update_#{issuable.to_ability_name}", issuable)
end
command :close do
@updates[:state_event] = 'close'
end
desc do
"Reopen this #{issuable.to_ability_name.humanize(capitalize: false)}"
end
condition do
issuable.persisted? &&
issuable.closed? &&
current_user.can?(:"update_#{issuable.to_ability_name}", issuable)
end
command :reopen do
@updates[:state_event] = 'reopen'
end
desc 'Change title'
params '<New title>'
condition do
issuable.persisted? &&
current_user.can?(:"update_#{issuable.to_ability_name}", issuable)
end
command :title do |title_param|
@updates[:title] = title_param
end
desc 'Assign'
params '@user'
condition do
current_user.can?(:"admin_#{issuable.to_ability_name}", project)
end
command :assign do |assignee_param|
user = extract_references(assignee_param, :user).first
user ||= User.find_by(username: assignee_param)
@updates[:assignee_id] = user.id if user
end
desc 'Remove assignee'
condition do
issuable.persisted? &&
issuable.assignee_id? &&
current_user.can?(:"admin_#{issuable.to_ability_name}", project)
end
command :unassign do
@updates[:assignee_id] = nil
end
desc 'Set milestone'
params '%"milestone"'
condition do
current_user.can?(:"admin_#{issuable.to_ability_name}", project) &&
project.milestones.active.any?
end
command :milestone do |milestone_param|
milestone = extract_references(milestone_param, :milestone).first
milestone ||= project.milestones.find_by(title: milestone_param.strip)
@updates[:milestone_id] = milestone.id if milestone
end
desc 'Remove milestone'
condition do
issuable.persisted? &&
issuable.milestone_id? &&
current_user.can?(:"admin_#{issuable.to_ability_name}", project)
end
command :remove_milestone do
@updates[:milestone_id] = nil
end
desc 'Add label(s)'
params '~label1 ~"label 2"'
condition do
current_user.can?(:"admin_#{issuable.to_ability_name}", project) &&
project.labels.any?
end
command :label do |labels_param|
label_ids = find_label_ids(labels_param)
@updates[:add_label_ids] = label_ids unless label_ids.empty?
end
desc 'Remove all or specific label(s)'
params '~label1 ~"label 2"'
condition do
issuable.persisted? &&
issuable.labels.any? &&
current_user.can?(:"admin_#{issuable.to_ability_name}", project)
end
command :unlabel do |labels_param = nil|
if labels_param.present?
label_ids = find_label_ids(labels_param)
@updates[:remove_label_ids] = label_ids unless label_ids.empty?
else
@updates[:label_ids] = []
end
end
desc 'Replace all label(s)'
params '~label1 ~"label 2"'
condition do
issuable.persisted? &&
issuable.labels.any? &&
current_user.can?(:"admin_#{issuable.to_ability_name}", project)
end
command :relabel do |labels_param|
label_ids = find_label_ids(labels_param)
@updates[:label_ids] = label_ids unless label_ids.empty?
end
desc 'Add a todo'
condition do
issuable.persisted? &&
!TodoService.new.todo_exist?(issuable, current_user)
end
command :todo do
@updates[:todo_event] = 'add'
end
desc 'Mark todo as done'
condition do
issuable.persisted? &&
TodoService.new.todo_exist?(issuable, current_user)
end
command :done do
@updates[:todo_event] = 'done'
end
desc 'Subscribe'
condition do
issuable.persisted? &&
!issuable.subscribed?(current_user)
end
command :subscribe do
@updates[:subscription_event] = 'subscribe'
end
desc 'Unsubscribe'
condition do
issuable.persisted? &&
issuable.subscribed?(current_user)
end
command :unsubscribe do
@updates[:subscription_event] = 'unsubscribe'
end
desc 'Set due date'
params '<in 2 days | this Friday | December 31st>'
condition do
issuable.respond_to?(:due_date) &&
current_user.can?(:"update_#{issuable.to_ability_name}", issuable)
end
command :due do |due_date_param|
due_date = Chronic.parse(due_date_param).try(:to_date)
@updates[:due_date] = due_date if due_date
end
desc 'Remove due date'
condition do
issuable.persisted? &&
issuable.respond_to?(:due_date) &&
issuable.due_date? &&
current_user.can?(:"update_#{issuable.to_ability_name}", issuable)
end
command :remove_due_date do
@updates[:due_date] = nil
end
# This is a dummy command, so that it appears in the autocomplete commands
desc 'CC'
params '@user'
command :cc
def find_label_ids(labels_param)
label_ids_by_reference = extract_references(labels_param, :label).map(&:id)
labels_ids_by_name = @project.labels.where(name: labels_param.split).select(:id)
label_ids_by_reference | labels_ids_by_name
end
def extract_references(arg, type)
ext = Gitlab::ReferenceExtractor.new(project, current_user)
ext.analyze(arg, author: current_user)
ext.references(type)
end
end
end
...@@ -142,7 +142,11 @@ class TodoService ...@@ -142,7 +142,11 @@ class TodoService
# When user marks some todos as done # When user marks some todos as done
def mark_todos_as_done(todos, current_user) def mark_todos_as_done(todos, current_user)
todos = current_user.todos.where(id: todos.map(&:id)) unless todos.respond_to?(:update_all) mark_todos_as_done_by_ids(todos.select(&:id), current_user)
end
def mark_todos_as_done_by_ids(ids, current_user)
todos = current_user.todos.where(id: ids)
marked_todos = todos.update_all(state: :done) marked_todos = todos.update_all(state: :done)
current_user.update_todos_count_cache current_user.update_todos_count_cache
...@@ -155,6 +159,10 @@ class TodoService ...@@ -155,6 +159,10 @@ class TodoService
create_todos(current_user, attributes) create_todos(current_user, attributes)
end end
def todo_exist?(issuable, current_user)
TodosFinder.new(current_user).execute.exists?(target: issuable)
end
private private
def create_todos(users, attributes) def create_todos(users, attributes)
......
- reporter = abuse_report.reporter - reporter = abuse_report.reporter
- user = abuse_report.user - user = abuse_report.user
%tr %tr
%th.visible-xs-block.visible-sm-block
%strong User
%td %td
- if user - if user
= link_to user.name, user = link_to user.name, user
...@@ -9,6 +11,7 @@ ...@@ -9,6 +11,7 @@
- else - else
(removed) (removed)
%td %td
%strong.subheading.visible-xs-block.visible-sm-block Reported by
- if reporter - if reporter
= link_to reporter.name, reporter = link_to reporter.name, reporter
- else - else
...@@ -16,16 +19,16 @@ ...@@ -16,16 +19,16 @@
.light.small .light.small
= time_ago_with_tooltip(abuse_report.created_at) = time_ago_with_tooltip(abuse_report.created_at)
%td %td
%strong.subheading.visible-xs-block.visible-sm-block Message
.message
= markdown(abuse_report.message.squish!, pipeline: :single_line, author: reporter) = markdown(abuse_report.message.squish!, pipeline: :single_line, author: reporter)
%td %td
- if user - if user
= link_to 'Remove user & report', admin_abuse_report_path(abuse_report, remove_user: true), = link_to 'Remove user & report', admin_abuse_report_path(abuse_report, remove_user: true),
data: { confirm: "USER #{user.name} WILL BE REMOVED! Are you sure?" }, remote: true, method: :delete, class: "btn btn-xs btn-remove js-remove-tr" data: { confirm: "USER #{user.name} WILL BE REMOVED! Are you sure?" }, remote: true, method: :delete, class: "btn btn-sm btn-block btn-remove js-remove-tr"
%td
- if user && !user.blocked? - if user && !user.blocked?
= link_to 'Block user', block_admin_user_path(user), data: {confirm: 'USER WILL BE BLOCKED! Are you sure?'}, method: :put, class: "btn btn-xs" = link_to 'Block user', block_admin_user_path(user), data: {confirm: 'USER WILL BE BLOCKED! Are you sure?'}, method: :put, class: "btn btn-sm btn-block"
- else - else
.btn.btn-xs.disabled .btn.btn-sm.disabled.btn-block
Already Blocked Already Blocked
= link_to 'Remove report', [:admin, abuse_report], remote: true, method: :delete, class: "btn btn-xs btn-close js-remove-tr" = link_to 'Remove report', [:admin, abuse_report], remote: true, method: :delete, class: "btn btn-sm btn-block btn-close js-remove-tr"
- page_title "Abuse Reports" - page_title 'Abuse Reports'
%h3.page-title Abuse Reports %h3.page-title Abuse Reports
%hr %hr
- if @abuse_reports.present? .abuse-reports
- if @abuse_reports.present?
.table-holder .table-holder
%table.table %table.table
%thead %thead.hidden-sm.hidden-xs
%tr %tr
%th User %th User
%th Reported by %th Reported by
%th Message %th.wide Message
%th Primary action %th Action
%th
= render @abuse_reports = render @abuse_reports
= paginate @abuse_reports - else
- else .no-reports
%h4 There are no abuse reports %span.pull-left
There are no abuse reports!
.pull-left
= emoji_icon 'tada'
- project = @target_project || @project - project = @target_project || @project
- noteable_class = @noteable.class if @noteable.present? - noteable_type = @noteable.class if @noteable.present?
:javascript :javascript
GitLab.GfmAutoComplete.dataSource = "#{autocomplete_sources_namespace_project_path(project.namespace, project, type: noteable_class, type_id: params[:id])}" GitLab.GfmAutoComplete.dataSource = "#{autocomplete_sources_namespace_project_path(project.namespace, project, type: noteable_type, type_id: params[:id])}"
GitLab.GfmAutoComplete.cachedData = undefined; GitLab.GfmAutoComplete.cachedData = undefined;
GitLab.GfmAutoComplete.setup(); GitLab.GfmAutoComplete.setup();
...@@ -76,7 +76,7 @@ ...@@ -76,7 +76,7 @@
- if blob && blob.respond_to?(:text?) && blob_text_viewable?(blob) - if blob && blob.respond_to?(:text?) && blob_text_viewable?(blob)
%table.code.white %table.code.white
- diff_file.highlighted_diff_lines.each do |line| - diff_file.highlighted_diff_lines.each do |line|
= render "projects/diffs/line", line: line, diff_file: diff_file, plain: true = render "projects/diffs/line", line: line, diff_file: diff_file, plain: true, email: true
- else - else
No preview for this file type No preview for this file type
%br %br
...@@ -60,13 +60,38 @@ ...@@ -60,13 +60,38 @@
two-factor authentication app before a U2F device. That way you'll always be able to two-factor authentication app before a U2F device. That way you'll always be able to
log in - even when you're using an unsupported browser. log in - even when you're using an unsupported browser.
.col-lg-9 .col-lg-9
%p
- if @registration_key_handles.present?
= icon "check inverse", base: "circle", class: "text-success", text: "You have #{pluralize(@registration_key_handles.size, 'U2F device')} registered with GitLab."
- if @u2f_registration.errors.present? - if @u2f_registration.errors.present?
= form_errors(@u2f_registration) = form_errors(@u2f_registration)
= render "u2f/register" = render "u2f/register"
%hr
%h5 U2F Devices (#{@u2f_registrations.length})
- if @u2f_registrations.present?
.table-responsive
%table.table.table-bordered.u2f-registrations
%colgroup
%col{ width: "50%" }
%col{ width: "30%" }
%col{ width: "20%" }
%thead
%tr
%th Name
%th Registered On
%th
%tbody
- @u2f_registrations.each do |registration|
%tr
%td= registration.name.presence || "<no name set>"
%td= registration.created_at.to_date.to_s(:medium)
%td= link_to "Delete", profile_u2f_registration_path(registration), method: :delete, class: "btn btn-danger pull-right", data: { confirm: "Are you sure you want to delete this device? This action cannot be undone." }
- else
.settings-message.text-center
You don't have any U2F devices registered yet.
- if two_factor_skippable? - if two_factor_skippable?
:javascript :javascript
var button = "<a class='btn btn-xs btn-warning pull-right' data-method='patch' href='#{skip_profile_two_factor_auth_path}'>Configure it later</a>"; var button = "<a class='btn btn-xs btn-warning pull-right' data-method='patch' href='#{skip_profile_two_factor_auth_path}'>Configure it later</a>";
......
- supports_slash_commands = local_assigns.fetch(:supports_slash_commands, false)
.zen-backdrop .zen-backdrop
- classes << ' js-gfm-input js-autosize markdown-area' - classes << ' js-gfm-input js-autosize markdown-area'
- if defined?(f) && f - if defined?(f) && f
= f.text_area attr, class: classes, placeholder: placeholder = f.text_area attr, class: classes, placeholder: placeholder, data: { supports_slash_commands: supports_slash_commands }
- else - else
= text_area_tag attr, nil, class: classes, placeholder: placeholder = text_area_tag attr, nil, class: classes, placeholder: placeholder
%a.zen-control.zen-control-leave.js-zen-leave{ href: "#" } %a.zen-control.zen-control-leave.js-zen-leave{ href: "#" }
......
...@@ -10,10 +10,9 @@ ...@@ -10,10 +10,9 @@
\ \
- if editable_diff?(diff_file) - if editable_diff?(diff_file)
= edit_blob_link(@merge_request.source_project, - link_opts = @merge_request.id ? { from_merge_request_id: @merge_request.id } : {}
@merge_request.source_branch, diff_file.new_path, = edit_blob_link(@merge_request.source_project, @merge_request.source_branch, diff_file.new_path,
from_merge_request_id: @merge_request.id, blob: blob, link_opts: link_opts)
skip_visible_check: true)
= view_file_btn(diff_commit.id, diff_file.new_path, project) = view_file_btn(diff_commit.id, diff_file.new_path, project)
......
- email = local_assigns.fetch(:email, false)
- plain = local_assigns.fetch(:plain, false) - plain = local_assigns.fetch(:plain, false)
- type = line.type - type = line.type
- line_code = diff_file.line_code(line) unless plain - line_code = diff_file.line_code(line) unless plain
...@@ -22,4 +23,8 @@ ...@@ -22,4 +23,8 @@
= link_text = link_text
- else - else
%a{href: "##{line_code}", data: { linenumber: link_text }} %a{href: "##{line_code}", data: { linenumber: link_text }}
%td.line_content.noteable_line{ class: type, data: (diff_view_line_data(line_code, diff_file.position(line), type) unless plain) }= diff_line_content(line.text, type) %td.line_content.noteable_line{ class: type, data: (diff_view_line_data(line_code, diff_file.position(line), type) unless plain) }<
- if email
%pre= diff_line_content(line.text, type)
- else
= diff_line_content(line.text, type)
...@@ -20,7 +20,7 @@ ...@@ -20,7 +20,7 @@
.mr-compare.merge-request .mr-compare.merge-request
%ul.merge-request-tabs.nav-links.no-top.no-bottom %ul.merge-request-tabs.nav-links.no-top.no-bottom
%li.commits-tab %li.commits-tab
= link_to url_for(params), data: {target: 'div#commits', action: 'commits', toggle: 'tab'} do = link_to url_for(params), data: {target: 'div#commits', action: 'new', toggle: 'tab'} do
Commits Commits
%span.badge= @commits.size %span.badge= @commits.size
- if @pipeline - if @pipeline
...@@ -52,11 +52,8 @@ ...@@ -52,11 +52,8 @@
$('#merge_request_assignee_id').val("#{current_user.id}").trigger("change"); $('#merge_request_assignee_id').val("#{current_user.id}").trigger("change");
e.preventDefault(); e.preventDefault();
}); });
:javascript :javascript
var merge_request var merge_request = new MergeRequest({
merge_request = new MergeRequest({ action: "#{(@show_changes_tab ? 'diffs' : 'new')}",
action: 'new', setUrl: false
diffs_loaded: true,
commits_loaded: true
}); });
...@@ -10,8 +10,12 @@ ...@@ -10,8 +10,12 @@
= f.hidden_field :position = f.hidden_field :position
= render layout: 'projects/md_preview', locals: { preview_class: "md-preview", referenced_users: true } do = render layout: 'projects/md_preview', locals: { preview_class: "md-preview", referenced_users: true } do
= render 'projects/zen', f: f, attr: :note, classes: 'note-textarea js-note-text', placeholder: "Write a comment or drag your files here..." = render 'projects/zen', f: f,
= render 'projects/notes/hints' attr: :note,
classes: 'note-textarea js-note-text',
placeholder: "Write a comment or drag your files here...",
supports_slash_commands: true
= render 'projects/notes/hints', supports_slash_commands: true
.error-alert .error-alert
.note-form-actions.clearfix .note-form-actions.clearfix
......
- supports_slash_commands = local_assigns.fetch(:supports_slash_commands, false)
.comment-toolbar.clearfix .comment-toolbar.clearfix
.toolbar-text .toolbar-text
Styling with Styling with
= link_to 'Markdown', help_page_path('markdown/markdown'), target: '_blank', tabindex: -1 = link_to 'Markdown', help_page_path('markdown/markdown'), target: '_blank', tabindex: -1
is supported - if supports_slash_commands
and
= link_to 'slash commands', help_page_path('workflow/slash_commands'), target: '_blank', tabindex: -1
are
- else
is
supported
%button.toolbar-button.markdown-selector{ type: 'button', tabindex: '-1' } %button.toolbar-button.markdown-selector{ type: 'button', tabindex: '-1' }
= icon('file-image-o', class: 'toolbar-button-icon') = icon('file-image-o', class: 'toolbar-button-icon')
Attach a file Attach a file
...@@ -52,8 +52,9 @@ ...@@ -52,8 +52,9 @@
= render layout: 'projects/md_preview', locals: { preview_class: "md-preview", referenced_users: true } do = render layout: 'projects/md_preview', locals: { preview_class: "md-preview", referenced_users: true } do
= render 'projects/zen', f: f, attr: :description, = render 'projects/zen', f: f, attr: :description,
classes: 'note-textarea', classes: 'note-textarea',
placeholder: "Write a comment or drag your files here..." placeholder: "Write a comment or drag your files here...",
= render 'projects/notes/hints' supports_slash_commands: !issuable.persisted?
= render 'projects/notes/hints', supports_slash_commands: !issuable.persisted?
.clearfix .clearfix
.error-alert .error-alert
......
...@@ -28,9 +28,14 @@ ...@@ -28,9 +28,14 @@
%script#js-register-u2f-registered{ type: "text/template" } %script#js-register-u2f-registered{ type: "text/template" }
%div.row.append-bottom-10 %div.row.append-bottom-10
%p Your device was successfully set up! Click this button to register with the GitLab server. .col-md-12
%p Your device was successfully set up! Give it a name and register it with the GitLab server.
= form_tag(create_u2f_profile_two_factor_auth_path, method: :post) do = form_tag(create_u2f_profile_two_factor_auth_path, method: :post) do
= hidden_field_tag :device_response, nil, class: 'form-control', required: true, id: "js-device-response" .row.append-bottom-10
.col-md-3
= text_field_tag 'u2f_registration[name]', nil, class: 'form-control', placeholder: "Pick a name"
.col-md-3
= hidden_field_tag 'u2f_registration[device_response]', nil, class: 'form-control', required: true, id: "js-device-response"
= submit_tag "Register U2F Device", class: "btn btn-success" = submit_tag "Register U2F Device", class: "btn btn-success"
:javascript :javascript
......
...@@ -375,6 +375,8 @@ Rails.application.routes.draw do ...@@ -375,6 +375,8 @@ Rails.application.routes.draw do
patch :skip patch :skip
end end
end end
resources :u2f_registrations, only: [:destroy]
end end
end end
......
# See http://doc.gitlab.com/ce/development/migration_style_guide.html
# for more information on how to write migrations for GitLab.
class AddColumnNameToU2fRegistrations < ActiveRecord::Migration
include Gitlab::Database::MigrationHelpers
# Set this constant to true if this migration requires downtime.
DOWNTIME = false
# When a migration requires downtime you **must** uncomment the following
# constant and define a short and easy to understand explanation as to why the
# migration requires downtime.
# DOWNTIME_REASON = ''
# When using the methods "add_concurrent_index" or "add_column_with_default"
# you must disable the use of transactions as these methods can not run in an
# existing transaction. When using "add_concurrent_index" make sure that this
# method is the _only_ method called in the migration, any other changes
# should go in a separate migration. This ensures that upon failure _only_ the
# index creation fails and can be retried or reverted easily.
#
# To disable transactions uncomment the following line and remove these
# comments:
# disable_ddl_transaction!
def change
add_column :u2f_registrations, :name, :string
end
end
...@@ -1018,6 +1018,7 @@ ActiveRecord::Schema.define(version: 20160818205718) do ...@@ -1018,6 +1018,7 @@ ActiveRecord::Schema.define(version: 20160818205718) do
t.integer "user_id" t.integer "user_id"
t.datetime "created_at", null: false t.datetime "created_at", null: false
t.datetime "updated_at", null: false t.datetime "updated_at", null: false
t.string "name"
end end
add_index "u2f_registrations", ["key_handle"], name: "index_u2f_registrations_on_key_handle", using: :btree add_index "u2f_registrations", ["key_handle"], name: "index_u2f_registrations_on_key_handle", using: :btree
......
...@@ -26,6 +26,7 @@ following locations: ...@@ -26,6 +26,7 @@ following locations:
- [Open source license templates](licenses.md) - [Open source license templates](licenses.md)
- [Namespaces](namespaces.md) - [Namespaces](namespaces.md)
- [Notes](notes.md) (comments) - [Notes](notes.md) (comments)
- [Pipelines](pipelines.md)
- [Projects](projects.md) including setting Webhooks - [Projects](projects.md) including setting Webhooks
- [Project Access Requests](access_requests.md) - [Project Access Requests](access_requests.md)
- [Project Members](members.md) - [Project Members](members.md)
......
...@@ -532,3 +532,49 @@ Example response: ...@@ -532,3 +532,49 @@ Example response:
"user": null "user": null
} }
``` ```
## Play a build
Triggers a manual action to start a build.
```
POST /projects/:id/builds/:build_id/play
```
| Attribute | Type | Required | Description |
|------------|---------|----------|---------------------|
| `id` | integer | yes | The ID of a project |
| `build_id` | integer | yes | The ID of a build |
```
curl --request POST --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v3/projects/1/builds/1/play"
```
Example of response
```json
{
"commit": {
"author_email": "admin@example.com",
"author_name": "Administrator",
"created_at": "2015-12-24T16:51:14.000+01:00",
"id": "0ff3ae198f8601a285adcf5c0fff204ee6fba5fd",
"message": "Test the CI integration.",
"short_id": "0ff3ae19",
"title": "Test the CI integration."
},
"coverage": null,
"created_at": "2016-01-11T10:13:33.506Z",
"artifacts_file": null,
"finished_at": null,
"id": 69,
"name": "rubocop",
"ref": "master",
"runner": null,
"stage": "test",
"started_at": null,
"status": "started",
"tag": false,
"user": null
}
```
# Deployments API
## List project deployments
Get a list of deployments in a project.
```
GET /projects/:id/deployments
```
| Attribute | Type | Required | Description |
|-----------|---------|----------|---------------------|
| `id` | integer | yes | The ID of a project |
```bash
curl --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v3/projects/1/deployments"
```
Example of response
```json
[
{
"created_at": "2016-08-11T07:36:40.222Z",
"deployable": {
"commit": {
"author_email": "admin@example.com",
"author_name": "Administrator",
"created_at": "2016-08-11T09:36:01.000+02:00",
"id": "99d03678b90d914dbb1b109132516d71a4a03ea8",
"message": "Merge branch 'new-title' into 'master'\r\n\r\nUpdate README\r\n\r\n\r\n\r\nSee merge request !1",
"short_id": "99d03678",
"title": "Merge branch 'new-title' into 'master'\r"
},
"coverage": null,
"created_at": "2016-08-11T07:36:27.357Z",
"finished_at": "2016-08-11T07:36:39.851Z",
"id": 657,
"name": "deploy",
"ref": "master",
"runner": null,
"stage": "deploy",
"started_at": null,
"status": "success",
"tag": false,
"user": {
"avatar_url": "http://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80&d=identicon",
"bio": null,
"created_at": "2016-08-11T07:09:20.351Z",
"id": 1,
"is_admin": true,
"linkedin": "",
"location": null,
"name": "Administrator",
"skype": "",
"state": "active",
"twitter": "",
"username": "root",
"web_url": "http://localhost:3000/u/root",
"website_url": ""
}
},
"environment": {
"external_url": "https://about.gitlab.com",
"id": 9,
"name": "production"
},
"id": 41,
"iid": 1,
"ref": "master",
"sha": "99d03678b90d914dbb1b109132516d71a4a03ea8",
"user": {
"avatar_url": "http://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80&d=identicon",
"id": 1,
"name": "Administrator",
"state": "active",
"username": "root",
"web_url": "http://localhost:3000/u/root"
}
},
{
"created_at": "2016-08-11T11:32:35.444Z",
"deployable": {
"commit": {
"author_email": "admin@example.com",
"author_name": "Administrator",
"created_at": "2016-08-11T13:28:26.000+02:00",
"id": "a91957a858320c0e17f3a0eca7cfacbff50ea29a",
"message": "Merge branch 'rename-readme' into 'master'\r\n\r\nRename README\r\n\r\n\r\n\r\nSee merge request !2",
"short_id": "a91957a8",
"title": "Merge branch 'rename-readme' into 'master'\r"
},
"coverage": null,
"created_at": "2016-08-11T11:32:24.456Z",
"finished_at": "2016-08-11T11:32:35.145Z",
"id": 664,
"name": "deploy",
"ref": "master",
"runner": null,
"stage": "deploy",
"started_at": null,
"status": "success",
"tag": false,
"user": {
"avatar_url": "http://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80&d=identicon",
"bio": null,
"created_at": "2016-08-11T07:09:20.351Z",
"id": 1,
"is_admin": true,
"linkedin": "",
"location": null,
"name": "Administrator",
"skype": "",
"state": "active",
"twitter": "",
"username": "root",
"web_url": "http://localhost:3000/u/root",
"website_url": ""
}
},
"environment": {
"external_url": "https://about.gitlab.com",
"id": 9,
"name": "production"
},
"id": 42,
"iid": 2,
"ref": "master",
"sha": "a91957a858320c0e17f3a0eca7cfacbff50ea29a",
"user": {
"avatar_url": "http://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80&d=identicon",
"id": 1,
"name": "Administrator",
"state": "active",
"username": "root",
"web_url": "http://localhost:3000/u/root"
}
}
]
```
## Get a specific deployment
```
GET /projects/:id/deployments/:deployment_id
```
| Attribute | Type | Required | Description |
|-----------|---------|----------|---------------------|
| `id` | integer | yes | The ID of a project |
| `deployment_id` | integer | yes | The ID of the deployment |
```bash
curl --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v3/projects/1/deployments/1"
```
Example of response
```json
{
"id": 42,
"iid": 2,
"ref": "master",
"sha": "a91957a858320c0e17f3a0eca7cfacbff50ea29a",
"created_at": "2016-08-11T11:32:35.444Z",
"user": {
"name": "Administrator",
"username": "root",
"id": 1,
"state": "active",
"avatar_url": "http://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80&d=identicon",
"web_url": "http://localhost:3000/u/root"
},
"environment": {
"id": 9,
"name": "production",
"external_url": "https://about.gitlab.com"
},
"deployable": {
"id": 664,
"status": "success",
"stage": "deploy",
"name": "deploy",
"ref": "master",
"tag": false,
"coverage": null,
"created_at": "2016-08-11T11:32:24.456Z",
"started_at": null,
"finished_at": "2016-08-11T11:32:35.145Z",
"user": {
"name": "Administrator",
"username": "root",
"id": 1,
"state": "active",
"avatar_url": "http://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80&d=identicon",
"web_url": "http://localhost:3000/u/root",
"created_at": "2016-08-11T07:09:20.351Z",
"is_admin": true,
"bio": null,
"location": null,
"skype": "",
"linkedin": "",
"twitter": "",
"website_url": ""
},
"commit": {
"id": "a91957a858320c0e17f3a0eca7cfacbff50ea29a",
"short_id": "a91957a8",
"title": "Merge branch 'rename-readme' into 'master'\r",
"author_name": "Administrator",
"author_email": "admin@example.com",
"created_at": "2016-08-11T13:28:26.000+02:00",
"message": "Merge branch 'rename-readme' into 'master'\r\n\r\nRename README\r\n\r\n\r\n\r\nSee merge request !2"
},
"runner": null
}
}
```
# Pipelines API
## List project pipelines
> [Introduced][ce-5837] in GitLab 8.11
```
GET /projects/:id/pipelines
```
| Attribute | Type | Required | Description |
|-----------|---------|----------|---------------------|
| `id` | integer | yes | The ID of a project |
```
curl --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v3/projects/1/pipelines"
```
Example of response
```json
[
{
"id": 47,
"status": "pending",
"ref": "new-pipeline",
"sha": "a91957a858320c0e17f3a0eca7cfacbff50ea29a",
"before_sha": "a91957a858320c0e17f3a0eca7cfacbff50ea29a",
"tag": false,
"yaml_errors": null,
"user": {
"name": "Administrator",
"username": "root",
"id": 1,
"state": "active",
"avatar_url": "http://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80&d=identicon",
"web_url": "http://localhost:3000/u/root"
},
"created_at": "2016-08-16T10:23:19.007Z",
"updated_at": "2016-08-16T10:23:19.216Z",
"started_at": null,
"finished_at": null,
"committed_at": null,
"duration": null
},
{
"id": 48,
"status": "pending",
"ref": "new-pipeline",
"sha": "eb94b618fb5865b26e80fdd8ae531b7a63ad851a",
"before_sha": "eb94b618fb5865b26e80fdd8ae531b7a63ad851a",
"tag": false,
"yaml_errors": null,
"user": {
"name": "Administrator",
"username": "root",
"id": 1,
"state": "active",
"avatar_url": "http://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80&d=identicon",
"web_url": "http://localhost:3000/u/root"
},
"created_at": "2016-08-16T10:23:21.184Z",
"updated_at": "2016-08-16T10:23:21.314Z",
"started_at": null,
"finished_at": null,
"committed_at": null,
"duration": null
}
]
```
## Get a single pipeline
> [Introduced][ce-5837] in GitLab 8.11
```
GET /projects/:id/pipelines/:pipeline_id
```
| Attribute | Type | Required | Description |
|------------|---------|----------|---------------------|
| `id` | integer | yes | The ID of a project |
| `pipeline_id` | integer | yes | The ID of a pipeline |
```
curl --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v3/projects/1/pipeline/46"
```
Example of response
```json
{
"id": 46,
"status": "success",
"ref": "master",
"sha": "a91957a858320c0e17f3a0eca7cfacbff50ea29a",
"before_sha": "a91957a858320c0e17f3a0eca7cfacbff50ea29a",
"tag": false,
"yaml_errors": null,
"user": {
"name": "Administrator",
"username": "root",
"id": 1,
"state": "active",
"avatar_url": "http://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80&d=identicon",
"web_url": "http://localhost:3000/u/root"
},
"created_at": "2016-08-11T11:28:34.085Z",
"updated_at": "2016-08-11T11:32:35.169Z",
"started_at": null,
"finished_at": "2016-08-11T11:32:35.145Z",
"committed_at": null,
"duration": null
}
```
## Retry failed builds in a pipeline
> [Introduced][ce-5837] in GitLab 8.11
```
POST /projects/:id/pipelines/:pipeline_id/retry
```
| Attribute | Type | Required | Description |
|------------|---------|----------|---------------------|
| `id` | integer | yes | The ID of a project |
| `pipeline_id` | integer | yes | The ID of a pipeline |
```
curl --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v3/projects/1/pipelines/46/retry"
```
Response:
```json
{
"id": 46,
"status": "pending",
"ref": "master",
"sha": "a91957a858320c0e17f3a0eca7cfacbff50ea29a",
"before_sha": "a91957a858320c0e17f3a0eca7cfacbff50ea29a",
"tag": false,
"yaml_errors": null,
"user": {
"name": "Administrator",
"username": "root",
"id": 1,
"state": "active",
"avatar_url": "http://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80&d=identicon",
"web_url": "http://localhost:3000/u/root"
},
"created_at": "2016-08-11T11:28:34.085Z",
"updated_at": "2016-08-11T11:32:35.169Z",
"started_at": null,
"finished_at": "2016-08-11T11:32:35.145Z",
"committed_at": null,
"duration": null
}
```
## Cancel a pipelines builds
> [Introduced][ce-5837] in GitLab 8.11
```
POST /projects/:id/pipelines/:pipeline_id/cancel
```
| Attribute | Type | Required | Description |
|------------|---------|----------|---------------------|
| `id` | integer | yes | The ID of a project |
| `pipeline_id` | integer | yes | The ID of a pipeline |
```
curl --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v3/projects/1/pipelines/46/cancel"
```
Response:
```json
{
"id": 46,
"status": "canceled",
"ref": "master",
"sha": "a91957a858320c0e17f3a0eca7cfacbff50ea29a",
"before_sha": "a91957a858320c0e17f3a0eca7cfacbff50ea29a",
"tag": false,
"yaml_errors": null,
"user": {
"name": "Administrator",
"username": "root",
"id": 1,
"state": "active",
"avatar_url": "http://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80&d=identicon",
"web_url": "http://localhost:3000/u/root"
},
"created_at": "2016-08-11T11:28:34.085Z",
"updated_at": "2016-08-11T11:32:35.169Z",
"started_at": null,
"finished_at": "2016-08-11T11:32:35.145Z",
"committed_at": null,
"duration": null
}
```
[ce-5837]: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/5837
...@@ -353,7 +353,7 @@ job_name: ...@@ -353,7 +353,7 @@ job_name:
| except | no | Defines a list of git refs for which build is not created | | except | no | Defines a list of git refs for which build is not created |
| tags | no | Defines a list of tags which are used to select Runner | | tags | no | Defines a list of tags which are used to select Runner |
| allow_failure | no | Allow build to fail. Failed build doesn't contribute to commit status | | allow_failure | no | Allow build to fail. Failed build doesn't contribute to commit status |
| when | no | Define when to run build. Can be `on_success`, `on_failure` or `always` | | when | no | Define when to run build. Can be `on_success`, `on_failure`, `always` or `manual` |
| dependencies | no | Define other builds that a build depends on so that you can pass artifacts between them| | dependencies | no | Define other builds that a build depends on so that you can pass artifacts between them|
| artifacts | no | Define list of build artifacts | | artifacts | no | Define list of build artifacts |
| cache | no | Define list of files that should be cached between subsequent runs | | cache | no | Define list of files that should be cached between subsequent runs |
......
...@@ -16,7 +16,7 @@ Subject to the terms and conditions of this Agreement, You hereby grant to GitLa ...@@ -16,7 +16,7 @@ Subject to the terms and conditions of this Agreement, You hereby grant to GitLa
Subject to the terms and conditions of this Agreement, You hereby grant to GitLab B.V. and to recipients of software distributed by GitLab B.V. a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by You that are necessarily infringed by Your Contribution(s) alone or by combination of Your Contribution(s) with the Work to which such Contribution(s) was submitted. If any entity institutes patent litigation against You or any other entity (including a cross-claim or counterclaim in a lawsuit) alleging that your Contribution, or the Work to which you have contributed, constitutes direct or contributory patent infringement, then any patent licenses granted to that entity under this Agreement for that Contribution or Work shall terminate as of the date such litigation is filed. Subject to the terms and conditions of this Agreement, You hereby grant to GitLab B.V. and to recipients of software distributed by GitLab B.V. a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by You that are necessarily infringed by Your Contribution(s) alone or by combination of Your Contribution(s) with the Work to which such Contribution(s) was submitted. If any entity institutes patent litigation against You or any other entity (including a cross-claim or counterclaim in a lawsuit) alleging that your Contribution, or the Work to which you have contributed, constitutes direct or contributory patent infringement, then any patent licenses granted to that entity under this Agreement for that Contribution or Work shall terminate as of the date such litigation is filed.
4. You represent that You are legally entitled to grant the above license. You represent further that each employee of the Corporation is authorized to submit Contributions on behalf of the Corporation, but excluding employees that are designated in writing by You as "Not authorized to submit Contributions on behalf of [name of corporation here]." 4. You represent that You are legally entitled to grant the above license. You represent further that each of Your employees is authorized to submit Contributions on Your behalf, but excluding employees that are designated in writing by You as "Not authorized to submit Contributions on behalf of [name of Your corporation here]." Such designations of exclusion for unauthorized employees are to be submitted via email to legal@gitlab.com.
5. You represent that each of Your Contributions is Your original creation (see section 7 for submissions on behalf of others). 5. You represent that each of Your Contributions is Your original creation (see section 7 for submissions on behalf of others).
...@@ -24,6 +24,6 @@ Subject to the terms and conditions of this Agreement, You hereby grant to GitLa ...@@ -24,6 +24,6 @@ Subject to the terms and conditions of this Agreement, You hereby grant to GitLa
7. Should You wish to submit work that is not Your original creation, You may submit it to GitLab B.V. separately from any Contribution, identifying the complete details of its source and of any license or other restriction (including, but not limited to, related patents, trademarks, and license agreements) of which you are personally aware, and conspicuously marking the work as "Submitted on behalf of a third-party: [named here]". 7. Should You wish to submit work that is not Your original creation, You may submit it to GitLab B.V. separately from any Contribution, identifying the complete details of its source and of any license or other restriction (including, but not limited to, related patents, trademarks, and license agreements) of which you are personally aware, and conspicuously marking the work as "Submitted on behalf of a third-party: [named here]".
8. It is your responsibility to notify GitLab B.V. when any change is required to the designation of employees not authorized to submit Contributions on behalf of the Corporation, or to the Corporation's Point of Contact with GitLab B.V.. 8. It is Your responsibility to notify GitLab.com when any change is required to the list of designated employees excluded from submitting Contributions on Your behalf per Section 4. Such notification should be sent via email to legal@gitlab.com.
This text is licensed under the [Creative Commons Attribution 3.0 License](https://creativecommons.org/licenses/by/3.0/) and the original source is the Google Open Source Programs Office. This text is licensed under the [Creative Commons Attribution 3.0 License](https://creativecommons.org/licenses/by/3.0/) and the original source is the Google Open Source Programs Office.
...@@ -7,6 +7,7 @@ ...@@ -7,6 +7,7 @@
- [GitLab Flow](gitlab_flow.md) - [GitLab Flow](gitlab_flow.md)
- [Groups](groups.md) - [Groups](groups.md)
- [Keyboard shortcuts](shortcuts.md) - [Keyboard shortcuts](shortcuts.md)
- [Slash commands](slash_commands.md)
- [File finder](file_finder.md) - [File finder](file_finder.md)
- [Labels](../user/project/labels.md) - [Labels](../user/project/labels.md)
- [Notification emails](notifications.md) - [Notification emails](notifications.md)
......
# GitLab slash commands
Slash commands are textual shortcuts for common actions on issues or merge
requests that are usually done by clicking buttons or dropdowns in GitLab's UI.
You can enter these commands while creating a new issue or merge request, and
in comments. Each command should be on a separate line in order to be properly
detected and executed. The commands are removed from the issue, merge request or
comment body before it is saved and will not be visible to anyone else.
Below is a list of all of the available commands and descriptions about what they
do.
| Command | Action |
|:---------------------------|:-------------|
| `/close` | Close the issue or merge request |
| `/reopen` | Reopen the issue or merge request |
| `/title <New title>` | Change title |
| `/assign @username` | Assign |
| `/unassign` | Remove assignee |
| `/milestone %milestone` | Set milestone |
| `/remove_milestone` | Remove milestone |
| `/label ~foo ~"bar baz"` | Add label(s) |
| `/unlabel ~foo ~"bar baz"` | Remove all or specific label(s) |
| `/relabel ~foo ~"bar baz"` | Replace all label(s) |
| `/todo` | Add a todo |
| `/done` | Mark todo as done |
| `/subscribe` | Subscribe |
| `/unsubscribe` | Unsubscribe |
| `/due <in 2 days | this Friday | December 31st>` | Set due date |
| `/remove_due_date` | Remove due date |
...@@ -48,7 +48,7 @@ class Spinach::Features::AwardEmoji < Spinach::FeatureSteps ...@@ -48,7 +48,7 @@ class Spinach::Features::AwardEmoji < Spinach::FeatureSteps
page.within '.awards' do page.within '.awards' do
expect(page).to have_selector '.js-emoji-btn' expect(page).to have_selector '.js-emoji-btn'
expect(page.find('.js-emoji-btn.active .js-counter')).to have_content '1' expect(page.find('.js-emoji-btn.active .js-counter')).to have_content '1'
expect(page).to have_css(".js-emoji-btn.active[data-original-title='me']") expect(page).to have_css(".js-emoji-btn.active[data-original-title='You']")
end end
end end
......
...@@ -299,7 +299,7 @@ class Spinach::Features::ProjectIssues < Spinach::FeatureSteps ...@@ -299,7 +299,7 @@ class Spinach::Features::ProjectIssues < Spinach::FeatureSteps
end end
step 'I fill in issue search with \'Rock and roll\'' do step 'I fill in issue search with \'Rock and roll\'' do
filter_issue 'Description for issue' filter_issue 'Rock and roll'
end end
step 'I should see \'Bugfix1\' in issues' do step 'I should see \'Bugfix1\' in issues' do
......
...@@ -43,6 +43,7 @@ module API ...@@ -43,6 +43,7 @@ module API
mount ::API::CommitStatuses mount ::API::CommitStatuses
mount ::API::Commits mount ::API::Commits
mount ::API::DeployKeys mount ::API::DeployKeys
mount ::API::Deployments
mount ::API::Environments mount ::API::Environments
mount ::API::Files mount ::API::Files
mount ::API::Groups mount ::API::Groups
...@@ -56,6 +57,7 @@ module API ...@@ -56,6 +57,7 @@ module API
mount ::API::Milestones mount ::API::Milestones
mount ::API::Namespaces mount ::API::Namespaces
mount ::API::Notes mount ::API::Notes
mount ::API::Pipelines
mount ::API::ProjectHooks mount ::API::ProjectHooks
mount ::API::ProjectSnippets mount ::API::ProjectSnippets
mount ::API::Projects mount ::API::Projects
......
...@@ -189,6 +189,27 @@ module API ...@@ -189,6 +189,27 @@ module API
present build, with: Entities::Build, present build, with: Entities::Build,
user_can_download_artifacts: can?(current_user, :read_build, user_project) user_can_download_artifacts: can?(current_user, :read_build, user_project)
end end
desc 'Trigger a manual build' do
success Entities::Build
detail 'This feature was added in GitLab 8.11'
end
params do
requires :build_id, type: Integer, desc: 'The ID of a Build'
end
post ":id/builds/:build_id/play" do
authorize_read_builds!
build = get_build!(params[:build_id])
bad_request!("Unplayable Build") unless build.playable?
build.play(current_user)
status 200
present build, with: Entities::Build,
user_can_download_artifacts: can?(current_user, :read_build, user_project)
end
end end
helpers do helpers do
......
module API
# Deployments RESTfull API endpoints
class Deployments < Grape::API
before { authenticate! }
params do
requires :id, type: String, desc: 'The project ID'
end
resource :projects do
desc 'Get all deployments of the project' do
detail 'This feature was introduced in GitLab 8.11.'
success Entities::Deployment
end
params do
optional :page, type: Integer, desc: 'Page number of the current request'
optional :per_page, type: Integer, desc: 'Number of items per page'
end
get ':id/deployments' do
authorize! :read_deployment, user_project
present paginate(user_project.deployments), with: Entities::Deployment
end
desc 'Gets a specific deployment' do
detail 'This feature was introduced in GitLab 8.11.'
success Entities::Deployment
end
params do
requires :deployment_id, type: Integer, desc: 'The deployment ID'
end
get ':id/deployments/:deployment_id' do
authorize! :read_deployment, user_project
deployment = user_project.deployments.find(params[:deployment_id])
present deployment, with: Entities::Deployment
end
end
end
end
...@@ -506,8 +506,28 @@ module API ...@@ -506,8 +506,28 @@ module API
expose :key, :value expose :key, :value
end end
class Pipeline < Grape::Entity
expose :id, :status, :ref, :sha, :before_sha, :tag, :yaml_errors
expose :user, with: Entities::UserBasic
expose :created_at, :updated_at, :started_at, :finished_at, :committed_at
expose :duration
end
class Environment < Grape::Entity class Environment < Grape::Entity
expose :id, :name, :external_url expose :id, :name, :external_url
expose :project, using: Entities::Project
end
class EnvironmentBasic < Grape::Entity
expose :id, :name, :external_url
end
class Deployment < Grape::Entity
expose :id, :iid, :ref, :sha, :created_at
expose :user, using: Entities::UserBasic
expose :environment, using: Entities::EnvironmentBasic
expose :deployable, using: Entities::Build
end end
class RepoLicense < Grape::Entity class RepoLicense < Grape::Entity
......
module API
class Pipelines < Grape::API
before { authenticate! }
params do
requires :id, type: String, desc: 'The project ID'
end
resource :projects do
desc 'Get all Pipelines of the project' do
detail 'This feature was introduced in GitLab 8.11.'
success Entities::Pipeline
end
params do
optional :page, type: Integer, desc: 'Page number of the current request'
optional :per_page, type: Integer, desc: 'Number of items per page'
end
get ':id/pipelines' do
authorize! :read_pipeline, user_project
present paginate(user_project.pipelines), with: Entities::Pipeline
end
desc 'Gets a specific pipeline for the project' do
detail 'This feature was introduced in GitLab 8.11'
success Entities::Pipeline
end
params do
requires :pipeline_id, type: Integer, desc: 'The pipeline ID'
end
get ':id/pipelines/:pipeline_id' do
authorize! :read_pipeline, user_project
present pipeline, with: Entities::Pipeline
end
desc 'Retry failed builds in the pipeline' do
detail 'This feature was introduced in GitLab 8.11.'
success Entities::Pipeline
end
params do
requires :pipeline_id, type: Integer, desc: 'The pipeline ID'
end
post ':id/pipelines/:pipeline_id/retry' do
authorize! :update_pipeline, user_project
pipeline.retry_failed(current_user)
present pipeline, with: Entities::Pipeline
end
desc 'Cancel all builds in the pipeline' do
detail 'This feature was introduced in GitLab 8.11.'
success Entities::Pipeline
end
params do
requires :pipeline_id, type: Integer, desc: 'The pipeline ID'
end
post ':id/pipelines/:pipeline_id/cancel' do
authorize! :update_pipeline, user_project
pipeline.cancel_running
status 200
present pipeline.reload, with: Entities::Pipeline
end
end
helpers do
def pipeline
@pipeline ||= user_project.pipelines.find(params[:pipeline_id])
end
end
end
end
...@@ -45,6 +45,7 @@ module Gitlab ...@@ -45,6 +45,7 @@ module Gitlab
def verify_record!(record:, invalid_exception:, record_name:) def verify_record!(record:, invalid_exception:, record_name:)
return if record.persisted? return if record.persisted?
return if record.errors.key?(:commands_only)
error_title = "The #{record_name} could not be created for the following reasons:" error_title = "The #{record_name} could not be created for the following reasons:"
......
module Gitlab
module SlashCommands
class CommandDefinition
attr_accessor :name, :aliases, :description, :params, :condition_block, :action_block
def initialize(name, attributes = {})
@name = name
@aliases = attributes[:aliases] || []
@description = attributes[:description] || ''
@params = attributes[:params] || []
@condition_block = attributes[:condition_block]
@action_block = attributes[:action_block]
end
def all_names
[name, *aliases]
end
def noop?
action_block.nil?
end
def available?(opts)
return true unless condition_block
context = OpenStruct.new(opts)
context.instance_exec(&condition_block)
end
def execute(context, opts, arg)
return if noop? || !available?(opts)
if arg.present?
context.instance_exec(arg, &action_block)
elsif action_block.arity == 0
context.instance_exec(&action_block)
end
end
def to_h(opts)
desc = description
if desc.respond_to?(:call)
context = OpenStruct.new(opts)
desc = context.instance_exec(&desc) rescue ''
end
{
name: name,
aliases: aliases,
description: desc,
params: params
}
end
end
end
end
module Gitlab
module SlashCommands
module Dsl
extend ActiveSupport::Concern
included do
cattr_accessor :command_definitions, instance_accessor: false do
[]
end
cattr_accessor :command_definitions_by_name, instance_accessor: false do
{}
end
end
class_methods do
# Allows to give a description to the next slash command.
# This description is shown in the autocomplete menu.
# It accepts a block that will be evaluated with the context given to
# `CommandDefintion#to_h`.
#
# Example:
#
# desc do
# "This is a dynamic description for #{noteable.to_ability_name}"
# end
# command :command_key do |arguments|
# # Awesome code block
# end
def desc(text = '', &block)
@description = block_given? ? block : text
end
# Allows to define params for the next slash command.
# These params are shown in the autocomplete menu.
#
# Example:
#
# params "~label ~label2"
# command :command_key do |arguments|
# # Awesome code block
# end
def params(*params)
@params = params
end
# Allows to define conditions that must be met in order for the command
# to be returned by `.command_names` & `.command_definitions`.
# It accepts a block that will be evaluated with the context given to
# `CommandDefintion#to_h`.
#
# Example:
#
# condition do
# project.public?
# end
# command :command_key do |arguments|
# # Awesome code block
# end
def condition(&block)
@condition_block = block
end
# Registers a new command which is recognizeable from body of email or
# comment.
# It accepts aliases and takes a block.
#
# Example:
#
# command :my_command, :alias_for_my_command do |arguments|
# # Awesome code block
# end
def command(*command_names, &block)
name, *aliases = command_names
definition = CommandDefinition.new(
name,
aliases: aliases,
description: @description,
params: @params,
condition_block: @condition_block,
action_block: block
)
self.command_definitions << definition
definition.all_names.each do |name|
self.command_definitions_by_name[name] = definition
end
@description = nil
@params = nil
@condition_block = nil
end
end
end
end
end
module Gitlab
module SlashCommands
# This class takes an array of commands that should be extracted from a
# given text.
#
# ```
# extractor = Gitlab::SlashCommands::Extractor.new([:open, :assign, :labels])
# ```
class Extractor
attr_reader :command_definitions
def initialize(command_definitions)
@command_definitions = command_definitions
end
# Extracts commands from content and return an array of commands.
# The array looks like the following:
# [
# ['command1'],
# ['command3', 'arg1 arg2'],
# ]
# The command and the arguments are stripped.
# The original command text is removed from the given `content`.
#
# Usage:
# ```
# extractor = Gitlab::SlashCommands::Extractor.new([:open, :assign, :labels])
# msg = %(hello\n/labels ~foo ~"bar baz"\nworld)
# commands = extractor.extract_commands(msg) #=> [['labels', '~foo ~"bar baz"']]
# msg #=> "hello\nworld"
# ```
def extract_commands(content, opts = {})
return [content, []] unless content
content = content.dup
commands = []
content.delete!("\r")
content.gsub!(commands_regex(opts)) do
if $~[:cmd]
commands << [$~[:cmd], $~[:arg]].reject(&:blank?)
''
else
$~[0]
end
end
[content.strip, commands]
end
private
# Builds a regular expression to match known commands.
# First match group captures the command name and
# second match group captures its arguments.
#
# It looks something like:
#
# /^\/(?<cmd>close|reopen|...)(?:( |$))(?<arg>[^\/\n]*)(?:\n|$)/
def commands_regex(opts)
names = command_names(opts).map(&:to_s)
@commands_regex ||= %r{
(?<code>
# Code blocks:
# ```
# Anything, including `/cmd arg` which are ignored by this filter
# ```
^```
.+?
\n```$
)
|
(?<html>
# HTML block:
# <tag>
# Anything, including `/cmd arg` which are ignored by this filter
# </tag>
^<[^>]+?>\n
.+?
\n<\/[^>]+?>$
)
|
(?<html>
# Quote block:
# >>>
# Anything, including `/cmd arg` which are ignored by this filter
# >>>
^>>>
.+?
\n>>>$
)
|
(?:
# Command not in a blockquote, blockcode, or HTML tag:
# /close
^\/
(?<cmd>#{Regexp.union(names)})
(?:
[ ]
(?<arg>[^\/\n]*)
)?
(?:\n|$)
)
}mx
end
def command_names(opts)
command_definitions.flat_map do |command|
next if command.noop?
command.all_names
end.compact
end
end
end
end
# == Schema Information
#
# Table name: commits
#
# id :integer not null, primary key
# project_id :integer
# ref :string(255)
# sha :string(255)
# before_sha :string(255)
# push_data :text
# created_at :datetime
# updated_at :datetime
# tag :boolean default(FALSE)
# yaml_errors :text
# committed_at :datetime
# gl_project_id :integer
#
FactoryGirl.define do FactoryGirl.define do
factory :ci_empty_pipeline, class: Ci::Pipeline do factory :ci_empty_pipeline, class: Ci::Pipeline do
ref 'master' ref 'master'
......
...@@ -147,6 +147,7 @@ describe 'Issue Boards', feature: true, js: true do ...@@ -147,6 +147,7 @@ describe 'Issue Boards', feature: true, js: true do
expect(page).to have_selector('.card', count: 20) expect(page).to have_selector('.card', count: 20)
evaluate_script("document.querySelectorAll('.board .board-list')[0].scrollTop = document.querySelectorAll('.board .board-list')[0].scrollHeight") evaluate_script("document.querySelectorAll('.board .board-list')[0].scrollTop = document.querySelectorAll('.board .board-list')[0].scrollHeight")
wait_for_vue_resource(spinner: false)
expect(page.find('.board-header')).to have_content('40') expect(page.find('.board-header')).to have_content('40')
expect(page).to have_selector('.card', count: 40) expect(page).to have_selector('.card', count: 40)
...@@ -165,6 +166,8 @@ describe 'Issue Boards', feature: true, js: true do ...@@ -165,6 +166,8 @@ describe 'Issue Boards', feature: true, js: true do
page.within(find('.board', match: :first)) do page.within(find('.board', match: :first)) do
find('.form-control').set issue1.title find('.form-control').set issue1.title
wait_for_vue_resource(spinner: false)
expect(page).to have_selector('.card', count: 1) expect(page).to have_selector('.card', count: 1)
end end
end end
...@@ -176,7 +179,11 @@ describe 'Issue Boards', feature: true, js: true do ...@@ -176,7 +179,11 @@ describe 'Issue Boards', feature: true, js: true do
expect(page).to have_selector('.card', count: 1) expect(page).to have_selector('.card', count: 1)
find('.board-search-clear-btn').click find('.board-search-clear-btn').click
end
wait_for_vue_resource
page.within(find('.board', match: :first)) do
expect(page).to have_selector('.card', count: 6) expect(page).to have_selector('.card', count: 6)
end end
end end
...@@ -189,6 +196,8 @@ describe 'Issue Boards', feature: true, js: true do ...@@ -189,6 +196,8 @@ describe 'Issue Boards', feature: true, js: true do
expect(page).to have_selector('.card', count: 5) expect(page).to have_selector('.card', count: 5)
end end
wait_for_vue_resource
page.within(find('.board:nth-child(2)')) do page.within(find('.board:nth-child(2)')) do
expect(page.find('.board-header')).to have_content('3') expect(page.find('.board-header')).to have_content('3')
expect(page).to have_selector('.card', count: 3) expect(page).to have_selector('.card', count: 3)
...@@ -263,6 +272,7 @@ describe 'Issue Boards', feature: true, js: true do ...@@ -263,6 +272,7 @@ describe 'Issue Boards', feature: true, js: true do
context 'new list' do context 'new list' do
it 'shows all labels in new list dropdown' do it 'shows all labels in new list dropdown' do
click_button 'Create new list' click_button 'Create new list'
wait_for_ajax
page.within('.dropdown-menu-issues-board-new') do page.within('.dropdown-menu-issues-board-new') do
expect(page).to have_content(planning.title) expect(page).to have_content(planning.title)
...@@ -273,6 +283,7 @@ describe 'Issue Boards', feature: true, js: true do ...@@ -273,6 +283,7 @@ describe 'Issue Boards', feature: true, js: true do
it 'creates new list for label' do it 'creates new list for label' do
click_button 'Create new list' click_button 'Create new list'
wait_for_ajax
page.within('.dropdown-menu-issues-board-new') do page.within('.dropdown-menu-issues-board-new') do
click_link testing.title click_link testing.title
...@@ -285,6 +296,7 @@ describe 'Issue Boards', feature: true, js: true do ...@@ -285,6 +296,7 @@ describe 'Issue Boards', feature: true, js: true do
it 'creates new list for Backlog label' do it 'creates new list for Backlog label' do
click_button 'Create new list' click_button 'Create new list'
wait_for_ajax
page.within('.dropdown-menu-issues-board-new') do page.within('.dropdown-menu-issues-board-new') do
click_link backlog.title click_link backlog.title
...@@ -297,6 +309,7 @@ describe 'Issue Boards', feature: true, js: true do ...@@ -297,6 +309,7 @@ describe 'Issue Boards', feature: true, js: true do
it 'creates new list for Done label' do it 'creates new list for Done label' do
click_button 'Create new list' click_button 'Create new list'
wait_for_ajax
page.within('.dropdown-menu-issues-board-new') do page.within('.dropdown-menu-issues-board-new') do
click_link done.title click_link done.title
...@@ -314,6 +327,7 @@ describe 'Issue Boards', feature: true, js: true do ...@@ -314,6 +327,7 @@ describe 'Issue Boards', feature: true, js: true do
end end
click_button 'Create new list' click_button 'Create new list'
wait_for_ajax
page.within('.dropdown-menu-issues-board-new') do page.within('.dropdown-menu-issues-board-new') do
click_link testing.title click_link testing.title
...@@ -333,6 +347,7 @@ describe 'Issue Boards', feature: true, js: true do ...@@ -333,6 +347,7 @@ describe 'Issue Boards', feature: true, js: true do
it 'filters by author' do it 'filters by author' do
page.within '.issues-filters' do page.within '.issues-filters' do
click_button('Author') click_button('Author')
wait_for_ajax
page.within '.dropdown-menu-author' do page.within '.dropdown-menu-author' do
click_link(user2.name) click_link(user2.name)
...@@ -358,6 +373,7 @@ describe 'Issue Boards', feature: true, js: true do ...@@ -358,6 +373,7 @@ describe 'Issue Boards', feature: true, js: true do
it 'filters by assignee' do it 'filters by assignee' do
page.within '.issues-filters' do page.within '.issues-filters' do
click_button('Assignee') click_button('Assignee')
wait_for_ajax
page.within '.dropdown-menu-assignee' do page.within '.dropdown-menu-assignee' do
click_link(user.name) click_link(user.name)
...@@ -383,6 +399,7 @@ describe 'Issue Boards', feature: true, js: true do ...@@ -383,6 +399,7 @@ describe 'Issue Boards', feature: true, js: true do
it 'filters by milestone' do it 'filters by milestone' do
page.within '.issues-filters' do page.within '.issues-filters' do
click_button('Milestone') click_button('Milestone')
wait_for_ajax
page.within '.milestone-filter' do page.within '.milestone-filter' do
click_link(milestone.title) click_link(milestone.title)
...@@ -408,6 +425,7 @@ describe 'Issue Boards', feature: true, js: true do ...@@ -408,6 +425,7 @@ describe 'Issue Boards', feature: true, js: true do
it 'filters by label' do it 'filters by label' do
page.within '.issues-filters' do page.within '.issues-filters' do
click_button('Label') click_button('Label')
wait_for_ajax
page.within '.dropdown-menu-labels' do page.within '.dropdown-menu-labels' do
click_link(testing.title) click_link(testing.title)
...@@ -436,6 +454,7 @@ describe 'Issue Boards', feature: true, js: true do ...@@ -436,6 +454,7 @@ describe 'Issue Boards', feature: true, js: true do
page.within '.issues-filters' do page.within '.issues-filters' do
click_button('Label') click_button('Label')
wait_for_ajax
page.within '.dropdown-menu-labels' do page.within '.dropdown-menu-labels' do
click_link(testing.title) click_link(testing.title)
...@@ -460,8 +479,9 @@ describe 'Issue Boards', feature: true, js: true do ...@@ -460,8 +479,9 @@ describe 'Issue Boards', feature: true, js: true do
it 'filters by multiple labels' do it 'filters by multiple labels' do
page.within '.issues-filters' do page.within '.issues-filters' do
click_button('Label') click_button('Label')
wait_for_ajax
page.within '.dropdown-menu-labels' do page.within(find('.dropdown-menu-labels')) do
click_link(testing.title) click_link(testing.title)
wait_for_vue_resource(spinner: false) wait_for_vue_resource(spinner: false)
click_link(bug.title) click_link(bug.title)
...@@ -486,6 +506,7 @@ describe 'Issue Boards', feature: true, js: true do ...@@ -486,6 +506,7 @@ describe 'Issue Boards', feature: true, js: true do
it 'filters by no label' do it 'filters by no label' do
page.within '.issues-filters' do page.within '.issues-filters' do
click_button('Label') click_button('Label')
wait_for_ajax
page.within '.dropdown-menu-labels' do page.within '.dropdown-menu-labels' do
click_link("No Label") click_link("No Label")
...@@ -510,10 +531,13 @@ describe 'Issue Boards', feature: true, js: true do ...@@ -510,10 +531,13 @@ describe 'Issue Boards', feature: true, js: true do
it 'filters by clicking label button on issue' do it 'filters by clicking label button on issue' do
page.within(find('.board', match: :first)) do page.within(find('.board', match: :first)) do
expect(page).to have_selector('.card', count: 6) expect(page).to have_selector('.card', count: 6)
expect(find('.card', match: :first)).to have_content(bug.title)
click_button(bug.title) click_button(bug.title)
wait_for_vue_resource(spinner: false) wait_for_vue_resource(spinner: false)
end end
wait_for_vue_resource
page.within(find('.board', match: :first)) do page.within(find('.board', match: :first)) do
expect(page.find('.board-header')).to have_content('1') expect(page.find('.board-header')).to have_content('1')
expect(page).to have_selector('.card', count: 1) expect(page).to have_selector('.card', count: 1)
......
require 'rails_helper'
feature 'Issues > User uses slash commands', feature: true, js: true do
include WaitForAjax
it_behaves_like 'issuable record that supports slash commands in its description and notes', :issue do
let(:issuable) { create(:issue, project: project) }
end
describe 'issue-only commands' do
let(:user) { create(:user) }
let(:project) { create(:project, :public) }
before do
project.team << [user, :master]
login_with(user)
visit namespace_project_issue_path(project.namespace, project, issue)
end
describe 'adding a due date from note' do
let(:issue) { create(:issue, project: project) }
it 'does not create a note, and sets the due date accordingly' do
page.within('.js-main-target-form') do
fill_in 'note[note]', with: "/due 2016-08-28"
click_button 'Comment'
end
expect(page).not_to have_content '/due 2016-08-28'
expect(page).to have_content 'Your commands have been executed!'
issue.reload
expect(issue.due_date).to eq Date.new(2016, 8, 28)
end
end
describe 'removing a due date from note' do
let(:issue) { create(:issue, project: project, due_date: Date.new(2016, 8, 28)) }
it 'does not create a note, and removes the due date accordingly' do
expect(issue.due_date).to eq Date.new(2016, 8, 28)
page.within('.js-main-target-form') do
fill_in 'note[note]', with: "/remove_due_date"
click_button 'Comment'
end
expect(page).not_to have_content '/remove_due_date'
expect(page).to have_content 'Your commands have been executed!'
issue.reload
expect(issue.due_date).to be_nil
end
end
end
end
...@@ -49,14 +49,14 @@ feature 'Create New Merge Request', feature: true, js: true do ...@@ -49,14 +49,14 @@ feature 'Create New Merge Request', feature: true, js: true do
click_link 'Changes' click_link 'Changes'
expect(page.find_link('Inline')[:class]).to match(/\bactive\b/) expect(page).to have_css('a.btn.active', text: 'Inline')
expect(page.find_link('Side-by-side')[:class]).not_to match(/\bactive\b/) expect(page).not_to have_css('a.btn.active', text: 'Side-by-side')
click_link 'Side-by-side' click_link 'Side-by-side'
click_link 'Changes' within '.merge-request' do
expect(page).not_to have_css('a.btn.active', text: 'Inline')
expect(page.find_link('Inline')[:class]).not_to match(/\bactive\b/) expect(page).to have_css('a.btn.active', text: 'Side-by-side')
expect(page.find_link('Side-by-side')[:class]).to match(/\bactive\b/) end
end end
end end
require 'rails_helper'
feature 'Merge Requests > User uses slash commands', feature: true, js: true do
include WaitForAjax
let(:user) { create(:user) }
let(:project) { create(:project, :public) }
let(:merge_request) { create(:merge_request, source_project: project) }
let!(:milestone) { create(:milestone, project: project, title: 'ASAP') }
it_behaves_like 'issuable record that supports slash commands in its description and notes', :merge_request do
let(:issuable) { create(:merge_request, source_project: project) }
let(:new_url_opts) { { merge_request: { source_branch: 'feature' } } }
end
describe 'adding a due date from note' do
before do
project.team << [user, :master]
login_with(user)
visit namespace_project_merge_request_path(project.namespace, project, merge_request)
end
it 'does not recognize the command nor create a note' do
page.within('.js-main-target-form') do
fill_in 'note[note]', with: "/due 2016-08-28"
click_button 'Comment'
end
expect(page).not_to have_content '/due 2016-08-28'
end
end
end
...@@ -12,10 +12,12 @@ feature 'Using U2F (Universal 2nd Factor) Devices for Authentication', feature: ...@@ -12,10 +12,12 @@ feature 'Using U2F (Universal 2nd Factor) Devices for Authentication', feature:
end end
def register_u2f_device(u2f_device = nil) def register_u2f_device(u2f_device = nil)
u2f_device ||= FakeU2fDevice.new(page) name = FFaker::Name.first_name
u2f_device ||= FakeU2fDevice.new(page, name)
u2f_device.respond_to_u2f_registration u2f_device.respond_to_u2f_registration
click_on 'Setup New U2F Device' click_on 'Setup New U2F Device'
expect(page).to have_content('Your device was successfully set up') expect(page).to have_content('Your device was successfully set up')
fill_in "Pick a name", with: name
click_on 'Register U2F Device' click_on 'Register U2F Device'
u2f_device u2f_device
end end
...@@ -40,13 +42,14 @@ feature 'Using U2F (Universal 2nd Factor) Devices for Authentication', feature: ...@@ -40,13 +42,14 @@ feature 'Using U2F (Universal 2nd Factor) Devices for Authentication', feature:
end end
describe 'when 2FA via OTP is enabled' do describe 'when 2FA via OTP is enabled' do
it 'allows registering a new device' do it 'allows registering a new device with a name' do
visit profile_account_path visit profile_account_path
manage_two_factor_authentication manage_two_factor_authentication
expect(page.body).to match("You've already enabled two-factor authentication using mobile") expect(page.body).to match("You've already enabled two-factor authentication using mobile")
register_u2f_device u2f_device = register_u2f_device
expect(page.body).to match(u2f_device.name)
expect(page.body).to match('Your U2F device was registered') expect(page.body).to match('Your U2F device was registered')
end end
...@@ -55,15 +58,31 @@ feature 'Using U2F (Universal 2nd Factor) Devices for Authentication', feature: ...@@ -55,15 +58,31 @@ feature 'Using U2F (Universal 2nd Factor) Devices for Authentication', feature:
# First device # First device
manage_two_factor_authentication manage_two_factor_authentication
register_u2f_device first_device = register_u2f_device
expect(page.body).to match('Your U2F device was registered') expect(page.body).to match('Your U2F device was registered')
# Second device # Second device
manage_two_factor_authentication second_device = register_u2f_device
register_u2f_device
expect(page.body).to match('Your U2F device was registered') expect(page.body).to match('Your U2F device was registered')
expect(page.body).to match(first_device.name)
expect(page.body).to match(second_device.name)
expect(U2fRegistration.count).to eq(2)
end
it 'allows deleting a device' do
visit profile_account_path
manage_two_factor_authentication manage_two_factor_authentication
expect(page.body).to match('You have 2 U2F devices registered') expect(page.body).to match("You've already enabled two-factor authentication using mobile")
first_u2f_device = register_u2f_device
second_u2f_device = register_u2f_device
click_on "Delete", match: :first
expect(page.body).to match('Successfully deleted')
expect(page.body).not_to match(first_u2f_device.name)
expect(page.body).to match(second_u2f_device.name)
end end
end end
...@@ -208,7 +227,7 @@ feature 'Using U2F (Universal 2nd Factor) Devices for Authentication', feature: ...@@ -208,7 +227,7 @@ feature 'Using U2F (Universal 2nd Factor) Devices for Authentication', feature:
describe "when a given U2F device has not been registered" do describe "when a given U2F device has not been registered" do
it "does not allow logging in with that particular device" do it "does not allow logging in with that particular device" do
unregistered_device = FakeU2fDevice.new(page) unregistered_device = FakeU2fDevice.new(page, FFaker::Name.first_name)
login_as(user) login_as(user)
unregistered_device.respond_to_u2f_authentication unregistered_device.respond_to_u2f_authentication
click_on "Login Via U2F Device" click_on "Login Via U2F Device"
...@@ -262,6 +281,7 @@ feature 'Using U2F (Universal 2nd Factor) Devices for Authentication', feature: ...@@ -262,6 +281,7 @@ feature 'Using U2F (Universal 2nd Factor) Devices for Authentication', feature:
end end
it "deletes u2f registrations" do it "deletes u2f registrations" do
visit profile_account_path
expect { click_on "Disable" }.to change { U2fRegistration.count }.by(-1) expect { click_on "Disable" }.to change { U2fRegistration.count }.by(-1)
end end
end end
......
Return-Path: <jake@adventuretime.ooo>
Received: from iceking.adventuretime.ooo ([unix socket]) by iceking (Cyrus v2.2.13-Debian-2.2.13-19+squeeze3) with LMTPA; Thu, 13 Jun 2013 17:03:50 -0400
Received: from mail-ie0-x234.google.com (mail-ie0-x234.google.com [IPv6:2607:f8b0:4001:c03::234]) by iceking.adventuretime.ooo (8.14.3/8.14.3/Debian-9.4) with ESMTP id r5DL3nFJ016967 (version=TLSv1/SSLv3 cipher=RC4-SHA bits=128 verify=NOT) for <reply+59d8df8370b7e95c5a49fbf86aeb2c93@appmail.adventuretime.ooo>; Thu, 13 Jun 2013 17:03:50 -0400
Received: by mail-ie0-f180.google.com with SMTP id f4so21977375iea.25 for <reply+59d8df8370b7e95c5a49fbf86aeb2c93@appmail.adventuretime.ooo>; Thu, 13 Jun 2013 14:03:48 -0700
Received: by 10.0.0.1 with HTTP; Thu, 13 Jun 2013 14:03:48 -0700
Date: Thu, 13 Jun 2013 17:03:48 -0400
From: Jake the Dog <jake@adventuretime.ooo>
To: reply+59d8df8370b7e95c5a49fbf86aeb2c93@appmail.adventuretime.ooo
Message-ID: <CADkmRc+rNGAGGbV2iE5p918UVy4UyJqVcXRO2=otppgzduJSg@mail.gmail.com>
In-Reply-To: <issue_1@localhost>
References: <issue_1@localhost> <reply-59d8df8370b7e95c5a49fbf86aeb2c93@localhost>
Subject: re: [Discourse Meta] eviltrout posted in 'Adventure Time Sux'
Mime-Version: 1.0
Content-Type: text/plain;
charset=ISO-8859-1
Content-Transfer-Encoding: 7bit
X-Sieve: CMU Sieve 2.2
X-Received: by 10.0.0.1 with SMTP id n7mr11234144ipb.85.1371157428600; Thu,
13 Jun 2013 14:03:48 -0700 (PDT)
X-Scanned-By: MIMEDefang 2.69 on IPv6:2001:470:1d:165::1
Cool!
/close
/todo
/due tomorrow
On Sun, Jun 9, 2013 at 1:39 PM, eviltrout via Discourse Meta
<reply+59d8df8370b7e95c5a49fbf86aeb2c93@appmail.adventuretime.ooo> wrote:
>
>
>
> eviltrout posted in 'Adventure Time Sux' on Discourse Meta:
>
> ---
> hey guys everyone knows adventure time sucks!
>
> ---
> Please visit this link to respond: http://localhost:3000/t/adventure-time-sux/1234/3
>
> To unsubscribe from these emails, visit your [user preferences](http://localhost:3000/user_preferences).
>
Return-Path: <jake@adventuretime.ooo>
Received: from iceking.adventuretime.ooo ([unix socket]) by iceking (Cyrus v2.2.13-Debian-2.2.13-19+squeeze3) with LMTPA; Thu, 13 Jun 2013 17:03:50 -0400
Received: from mail-ie0-x234.google.com (mail-ie0-x234.google.com [IPv6:2607:f8b0:4001:c03::234]) by iceking.adventuretime.ooo (8.14.3/8.14.3/Debian-9.4) with ESMTP id r5DL3nFJ016967 (version=TLSv1/SSLv3 cipher=RC4-SHA bits=128 verify=NOT) for <reply+59d8df8370b7e95c5a49fbf86aeb2c93@appmail.adventuretime.ooo>; Thu, 13 Jun 2013 17:03:50 -0400
Received: by mail-ie0-f180.google.com with SMTP id f4so21977375iea.25 for <reply+59d8df8370b7e95c5a49fbf86aeb2c93@appmail.adventuretime.ooo>; Thu, 13 Jun 2013 14:03:48 -0700
Received: by 10.0.0.1 with HTTP; Thu, 13 Jun 2013 14:03:48 -0700
Date: Thu, 13 Jun 2013 17:03:48 -0400
From: Jake the Dog <jake@adventuretime.ooo>
To: reply+59d8df8370b7e95c5a49fbf86aeb2c93@appmail.adventuretime.ooo
Message-ID: <CADkmRc+rNGAGGbV2iE5p918UVy4UyJqVcXRO2=otppgzduJSg@mail.gmail.com>
In-Reply-To: <issue_1@localhost>
References: <issue_1@localhost> <reply-59d8df8370b7e95c5a49fbf86aeb2c93@localhost>
Subject: re: [Discourse Meta] eviltrout posted in 'Adventure Time Sux'
Mime-Version: 1.0
Content-Type: text/plain;
charset=ISO-8859-1
Content-Transfer-Encoding: 7bit
X-Sieve: CMU Sieve 2.2
X-Received: by 10.0.0.1 with SMTP id n7mr11234144ipb.85.1371157428600; Thu,
13 Jun 2013 14:03:48 -0700 (PDT)
X-Scanned-By: MIMEDefang 2.69 on IPv6:2001:470:1d:165::1
/close
/todo
/due tomorrow
On Sun, Jun 9, 2013 at 1:39 PM, eviltrout via Discourse Meta
<reply+59d8df8370b7e95c5a49fbf86aeb2c93@appmail.adventuretime.ooo> wrote:
>
>
>
> eviltrout posted in 'Adventure Time Sux' on Discourse Meta:
>
> ---
> hey guys everyone knows adventure time sucks!
>
> ---
> Please visit this link to respond: http://localhost:3000/t/adventure-time-sux/1234/3
>
> To unsubscribe from these emails, visit your [user preferences](http://localhost:3000/user_preferences).
>
...@@ -69,18 +69,40 @@ describe BlobHelper do ...@@ -69,18 +69,40 @@ describe BlobHelper do
end end
describe "#edit_blob_link" do describe "#edit_blob_link" do
let(:project) { create(:project) } let(:namespace) { create(:namespace, name: 'gitlab' )}
let(:project) { create(:project, namespace: namespace) }
before do before do
allow(self).to receive(:current_user).and_return(double) allow(self).to receive(:current_user).and_return(double)
allow(self).to receive(:can_collaborate_with_project?).and_return(true)
end end
it 'verifies blob is text' do it 'verifies blob is text' do
expect(self).not_to receive(:blob_text_viewable?) expect(helper).not_to receive(:blob_text_viewable?)
button = edit_blob_link(project, 'refs/heads/master', 'README.md') button = edit_blob_link(project, 'refs/heads/master', 'README.md')
expect(button).to start_with('<button') expect(button).to start_with('<button')
end end
it 'uses the passed blob instead retrieve from repository' do
blob = project.repository.blob_at('refs/heads/master', 'README.md')
expect(project.repository).not_to receive(:blob_at)
edit_blob_link(project, 'refs/heads/master', 'README.md', blob: blob)
end
it 'returns a link with the proper route' do
link = edit_blob_link(project, 'master', 'README.md')
expect(Capybara.string(link).find_link('Edit')[:href]).to eq('/gitlab/gitlabhq/edit/master/README.md')
end
it 'returns a link with the passed link_opts on the expected route' do
link = edit_blob_link(project, 'master', 'README.md', link_opts: { mr_id: 10 })
expect(Capybara.string(link).find_link('Edit')[:href]).to eq('/gitlab/gitlabhq/edit/master/README.md?mr_id=10')
end
end end
end end
...@@ -62,6 +62,32 @@ describe IssuesHelper do ...@@ -62,6 +62,32 @@ describe IssuesHelper do
it { is_expected.to eq("!1, !2, or !3") } it { is_expected.to eq("!1, !2, or !3") }
end end
describe '#award_user_list' do
let!(:awards) { build_list(:award_emoji, 15) }
it "returns a comma seperated list of 1-9 users" do
expect(award_user_list(awards.first(9), nil)).to eq(awards.first(9).map { |a| a.user.name }.to_sentence)
end
it "displays the current user's name as 'You'" do
expect(award_user_list(awards.first(1), awards[0].user)).to eq('You')
end
it "truncates lists of larger than 9 users" do
expect(award_user_list(awards, nil)).to eq(awards.first(9).map { |a| a.user.name }.join(', ') + ", and 6 more.")
end
it "displays the current user in front of 0-9 other users" do
expect(award_user_list(awards, awards[0].user)).
to eq("You, " + awards[1..9].map { |a| a.user.name }.join(', ') + ", and 5 more.")
end
it "displays the current user in front regardless of position in the list" do
expect(award_user_list(awards, awards[12].user)).
to eq("You, " + awards[0..8].map { |a| a.user.name }.join(', ') + ", and 5 more.")
end
end
describe '#award_active_class' do describe '#award_active_class' do
let!(:upvote) { create(:award_emoji) } let!(:upvote) { create(:award_emoji) }
......
/*= require abuse_reports */
/*= require jquery */
((global) => {
const FIXTURE = 'abuse_reports.html';
const MAX_MESSAGE_LENGTH = 500;
function assertMaxLength($message) {
expect($message.text().length).toEqual(MAX_MESSAGE_LENGTH);
}
describe('Abuse Reports', function() {
fixture.preload(FIXTURE);
beforeEach(function() {
fixture.load(FIXTURE);
new global.AbuseReports();
});
it('should truncate long messages', function() {
const $longMessage = $('#long');
expect($longMessage.data('original-message')).toEqual(jasmine.anything());
assertMaxLength($longMessage);
});
it('should not truncate short messages', function() {
const $shortMessage = $('#short');
expect($shortMessage.data('original-message')).not.toEqual(jasmine.anything());
});
it('should allow clicking a truncated message to expand and collapse the full message', function() {
const $longMessage = $('#long');
$longMessage.click();
expect($longMessage.data('original-message').length).toEqual($longMessage.text().length);
$longMessage.click();
assertMaxLength($longMessage);
});
});
})(window.gl);
...@@ -143,6 +143,52 @@ ...@@ -143,6 +143,52 @@
return expect($votesBlock.find('[data-emoji=fire]').length).toBe(0); return expect($votesBlock.find('[data-emoji=fire]').length).toBe(0);
}); });
}); });
describe('::addYouToUserList', function() {
it('should prepend "You" to the award tooltip', function() {
var $thumbsUpEmoji, $votesBlock, awardUrl;
awardUrl = awardsHandler.getAwardUrl();
$votesBlock = $('.js-awards-block').eq(0);
$thumbsUpEmoji = $votesBlock.find('[data-emoji=thumbsup]').parent();
$thumbsUpEmoji.attr('data-title', 'sam, jerry, max, and andy');
awardsHandler.addAward($votesBlock, awardUrl, 'thumbsup', false);
$thumbsUpEmoji.tooltip();
return expect($thumbsUpEmoji.data("original-title")).toBe('You, sam, jerry, max, and andy');
});
return it('handles the special case where "You" is not cleanly comma seperated', function() {
var $thumbsUpEmoji, $votesBlock, awardUrl;
awardUrl = awardsHandler.getAwardUrl();
$votesBlock = $('.js-awards-block').eq(0);
$thumbsUpEmoji = $votesBlock.find('[data-emoji=thumbsup]').parent();
$thumbsUpEmoji.attr('data-title', 'sam');
awardsHandler.addAward($votesBlock, awardUrl, 'thumbsup', false);
$thumbsUpEmoji.tooltip();
return expect($thumbsUpEmoji.data("original-title")).toBe('You and sam');
});
});
describe('::removeYouToUserList', function() {
it('removes "You" from the front of the tooltip', function() {
var $thumbsUpEmoji, $votesBlock, awardUrl;
awardUrl = awardsHandler.getAwardUrl();
$votesBlock = $('.js-awards-block').eq(0);
$thumbsUpEmoji = $votesBlock.find('[data-emoji=thumbsup]').parent();
$thumbsUpEmoji.attr('data-title', 'You, sam, jerry, max, and andy');
$thumbsUpEmoji.addClass('active');
awardsHandler.addAward($votesBlock, awardUrl, 'thumbsup', false);
$thumbsUpEmoji.tooltip();
return expect($thumbsUpEmoji.data("original-title")).toBe('sam, jerry, max, and andy');
});
return it('handles the special case where "You" is not cleanly comma seperated', function() {
var $thumbsUpEmoji, $votesBlock, awardUrl;
awardUrl = awardsHandler.getAwardUrl();
$votesBlock = $('.js-awards-block').eq(0);
$thumbsUpEmoji = $votesBlock.find('[data-emoji=thumbsup]').parent();
$thumbsUpEmoji.attr('data-title', 'You and sam');
$thumbsUpEmoji.addClass('active');
awardsHandler.addAward($votesBlock, awardUrl, 'thumbsup', false);
$thumbsUpEmoji.tooltip();
return expect($thumbsUpEmoji.data("original-title")).toBe('sam');
});
});
describe('search', function() { describe('search', function() {
return it('should filter the emoji', function() { return it('should filter the emoji', function() {
$('.js-add-award').eq(0).click(); $('.js-add-award').eq(0).click();
......
.abuse-reports
.message#long
Cat ipsum dolor sit amet, hide head under blanket so no one can see.
Gate keepers of hell eat and than sleep on your face but hunt by meowing
loudly at 5am next to human slave food dispenser cats go for world
domination or chase laser, yet poop on grasses chirp at birds. Cat is love,
cat is life chase after silly colored fish toys around the house climb a
tree, wait for a fireman jump to fireman then scratch his face fall asleep
on the washing machine lies down always hungry so caticus cuteicus. Sit on
human. Spot something, big eyes, big eyes, crouch, shake butt, prepare to
pounce sleep in the bathroom sink hiss at vacuum cleaner hide head under
blanket so no one can see throwup on your pillow.
.message#short
Cat ipsum dolor sit amet, groom yourself 4 hours - checked, have your
beauty sleep 18 hours - checked, be fabulous for the rest of the day -
checked! for shake treat bag.
...@@ -60,6 +60,67 @@ describe Gitlab::Email::Handler::CreateNoteHandler, lib: true do ...@@ -60,6 +60,67 @@ describe Gitlab::Email::Handler::CreateNoteHandler, lib: true do
it "raises an InvalidNoteError" do it "raises an InvalidNoteError" do
expect { receiver.execute }.to raise_error(Gitlab::Email::InvalidNoteError) expect { receiver.execute }.to raise_error(Gitlab::Email::InvalidNoteError)
end end
context 'because the note was commands only' do
let!(:email_raw) { fixture_file("emails/commands_only_reply.eml") }
context 'and current user cannot update noteable' do
it 'raises a CommandsOnlyNoteError' do
expect { receiver.execute }.to raise_error(Gitlab::Email::InvalidNoteError)
end
end
context 'and current user can update noteable' do
before do
project.team << [user, :developer]
end
it 'does not raise an error' do
expect(TodoService.new.todo_exist?(noteable, user)).to be_falsy
# One system note is created for the 'close' event
expect { receiver.execute }.to change { noteable.notes.count }.by(1)
expect(noteable.reload).to be_closed
expect(noteable.due_date).to eq(Date.tomorrow)
expect(TodoService.new.todo_exist?(noteable, user)).to be_truthy
end
end
end
end
context 'when the note contains slash commands' do
let!(:email_raw) { fixture_file("emails/commands_in_reply.eml") }
context 'and current user cannot update noteable' do
it 'post a note and does not update the noteable' do
expect(TodoService.new.todo_exist?(noteable, user)).to be_falsy
# One system note is created for the new note
expect { receiver.execute }.to change { noteable.notes.count }.by(1)
expect(noteable.reload).to be_open
expect(noteable.due_date).to be_nil
expect(TodoService.new.todo_exist?(noteable, user)).to be_falsy
end
end
context 'and current user can update noteable' do
before do
project.team << [user, :developer]
end
it 'post a note and updates the noteable' do
expect(TodoService.new.todo_exist?(noteable, user)).to be_falsy
# One system note is created for the new note, one for the 'close' event
expect { receiver.execute }.to change { noteable.notes.count }.by(2)
expect(noteable.reload).to be_closed
expect(noteable.due_date).to eq(Date.tomorrow)
expect(TodoService.new.todo_exist?(noteable, user)).to be_truthy
end
end
end end
context "when the reply is blank" do context "when the reply is blank" do
......
require 'spec_helper'
describe Gitlab::SlashCommands::CommandDefinition do
subject { described_class.new(:command) }
describe "#all_names" do
context "when the command has aliases" do
before do
subject.aliases = [:alias1, :alias2]
end
it "returns an array with the name and aliases" do
expect(subject.all_names).to eq([:command, :alias1, :alias2])
end
end
context "when the command doesn't have aliases" do
it "returns an array with the name" do
expect(subject.all_names).to eq([:command])
end
end
end
describe "#noop?" do
context "when the command has an action block" do
before do
subject.action_block = proc { }
end
it "returns false" do
expect(subject.noop?).to be false
end
end
context "when the command doesn't have an action block" do
it "returns true" do
expect(subject.noop?).to be true
end
end
end
describe "#available?" do
let(:opts) { { go: false } }
context "when the command has a condition block" do
before do
subject.condition_block = proc { go }
end
context "when the condition block returns true" do
before do
opts[:go] = true
end
it "returns true" do
expect(subject.available?(opts)).to be true
end
end
context "when the condition block returns false" do
it "returns false" do
expect(subject.available?(opts)).to be false
end
end
end
context "when the command doesn't have a condition block" do
it "returns true" do
expect(subject.available?(opts)).to be true
end
end
end
describe "#execute" do
let(:context) { OpenStruct.new(run: false) }
context "when the command is a noop" do
it "doesn't execute the command" do
expect(context).not_to receive(:instance_exec)
subject.execute(context, {}, nil)
expect(context.run).to be false
end
end
context "when the command is not a noop" do
before do
subject.action_block = proc { self.run = true }
end
context "when the command is not available" do
before do
subject.condition_block = proc { false }
end
it "doesn't execute the command" do
subject.execute(context, {}, nil)
expect(context.run).to be false
end
end
context "when the command is available" do
context "when the commnd has no arguments" do
before do
subject.action_block = proc { self.run = true }
end
context "when the command is provided an argument" do
it "executes the command" do
subject.execute(context, {}, true)
expect(context.run).to be true
end
end
context "when the command is not provided an argument" do
it "executes the command" do
subject.execute(context, {}, nil)
expect(context.run).to be true
end
end
end
context "when the command has 1 required argument" do
before do
subject.action_block = ->(arg) { self.run = arg }
end
context "when the command is provided an argument" do
it "executes the command" do
subject.execute(context, {}, true)
expect(context.run).to be true
end
end
context "when the command is not provided an argument" do
it "doesn't execute the command" do
subject.execute(context, {}, nil)
expect(context.run).to be false
end
end
end
context "when the command has 1 optional argument" do
before do
subject.action_block = proc { |arg = nil| self.run = arg || true }
end
context "when the command is provided an argument" do
it "executes the command" do
subject.execute(context, {}, true)
expect(context.run).to be true
end
end
context "when the command is not provided an argument" do
it "executes the command" do
subject.execute(context, {}, nil)
expect(context.run).to be true
end
end
end
end
end
end
end
require 'spec_helper'
describe Gitlab::SlashCommands::Dsl do
before :all do
DummyClass = Struct.new(:project) do
include Gitlab::SlashCommands::Dsl
desc 'A command with no args'
command :no_args, :none do
"Hello World!"
end
params 'The first argument'
command :one_arg, :once, :first do |arg1|
arg1
end
desc do
"A dynamic description for #{noteable.upcase}"
end
params 'The first argument', 'The second argument'
command :two_args do |arg1, arg2|
[arg1, arg2]
end
command :cc
condition do
project == 'foo'
end
command :cond_action do |arg|
arg
end
end
end
describe '.command_definitions' do
it 'returns an array with commands definitions' do
no_args_def, one_arg_def, two_args_def, cc_def, cond_action_def = DummyClass.command_definitions
expect(no_args_def.name).to eq(:no_args)
expect(no_args_def.aliases).to eq([:none])
expect(no_args_def.description).to eq('A command with no args')
expect(no_args_def.params).to eq([])
expect(no_args_def.condition_block).to be_nil
expect(no_args_def.action_block).to be_a_kind_of(Proc)
expect(one_arg_def.name).to eq(:one_arg)
expect(one_arg_def.aliases).to eq([:once, :first])
expect(one_arg_def.description).to eq('')
expect(one_arg_def.params).to eq(['The first argument'])
expect(one_arg_def.condition_block).to be_nil
expect(one_arg_def.action_block).to be_a_kind_of(Proc)
expect(two_args_def.name).to eq(:two_args)
expect(two_args_def.aliases).to eq([])
expect(two_args_def.to_h(noteable: "issue")[:description]).to eq('A dynamic description for ISSUE')
expect(two_args_def.params).to eq(['The first argument', 'The second argument'])
expect(two_args_def.condition_block).to be_nil
expect(two_args_def.action_block).to be_a_kind_of(Proc)
expect(cc_def.name).to eq(:cc)
expect(cc_def.aliases).to eq([])
expect(cc_def.description).to eq('')
expect(cc_def.params).to eq([])
expect(cc_def.condition_block).to be_nil
expect(cc_def.action_block).to be_nil
expect(cond_action_def.name).to eq(:cond_action)
expect(cond_action_def.aliases).to eq([])
expect(cond_action_def.description).to eq('')
expect(cond_action_def.params).to eq([])
expect(cond_action_def.condition_block).to be_a_kind_of(Proc)
expect(cond_action_def.action_block).to be_a_kind_of(Proc)
end
end
end
require 'spec_helper'
describe Gitlab::SlashCommands::Extractor do
let(:definitions) do
Class.new do
include Gitlab::SlashCommands::Dsl
command(:reopen, :open) { }
command(:assign) { }
command(:labels) { }
command(:power) { }
end.command_definitions
end
let(:extractor) { described_class.new(definitions) }
shared_examples 'command with no argument' do
it 'extracts command' do
msg, commands = extractor.extract_commands(original_msg)
expect(commands).to eq [['reopen']]
expect(msg).to eq final_msg
end
end
shared_examples 'command with a single argument' do
it 'extracts command' do
msg, commands = extractor.extract_commands(original_msg)
expect(commands).to eq [['assign', '@joe']]
expect(msg).to eq final_msg
end
end
shared_examples 'command with multiple arguments' do
it 'extracts command' do
msg, commands = extractor.extract_commands(original_msg)
expect(commands).to eq [['labels', '~foo ~"bar baz" label']]
expect(msg).to eq final_msg
end
end
describe '#extract_commands' do
describe 'command with no argument' do
context 'at the start of content' do
it_behaves_like 'command with no argument' do
let(:original_msg) { "/reopen\nworld" }
let(:final_msg) { "world" }
end
end
context 'in the middle of content' do
it_behaves_like 'command with no argument' do
let(:original_msg) { "hello\n/reopen\nworld" }
let(:final_msg) { "hello\nworld" }
end
end
context 'in the middle of a line' do
it 'does not extract command' do
msg = "hello\nworld /reopen"
msg, commands = extractor.extract_commands(msg)
expect(commands).to be_empty
expect(msg).to eq "hello\nworld /reopen"
end
end
context 'at the end of content' do
it_behaves_like 'command with no argument' do
let(:original_msg) { "hello\n/reopen" }
let(:final_msg) { "hello" }
end
end
end
describe 'command with a single argument' do
context 'at the start of content' do
it_behaves_like 'command with a single argument' do
let(:original_msg) { "/assign @joe\nworld" }
let(:final_msg) { "world" }
end
end
context 'in the middle of content' do
it_behaves_like 'command with a single argument' do
let(:original_msg) { "hello\n/assign @joe\nworld" }
let(:final_msg) { "hello\nworld" }
end
end
context 'in the middle of a line' do
it 'does not extract command' do
msg = "hello\nworld /assign @joe"
msg, commands = extractor.extract_commands(msg)
expect(commands).to be_empty
expect(msg).to eq "hello\nworld /assign @joe"
end
end
context 'at the end of content' do
it_behaves_like 'command with a single argument' do
let(:original_msg) { "hello\n/assign @joe" }
let(:final_msg) { "hello" }
end
end
context 'when argument is not separated with a space' do
it 'does not extract command' do
msg = "hello\n/assign@joe\nworld"
msg, commands = extractor.extract_commands(msg)
expect(commands).to be_empty
expect(msg).to eq "hello\n/assign@joe\nworld"
end
end
end
describe 'command with multiple arguments' do
context 'at the start of content' do
it_behaves_like 'command with multiple arguments' do
let(:original_msg) { %(/labels ~foo ~"bar baz" label\nworld) }
let(:final_msg) { "world" }
end
end
context 'in the middle of content' do
it_behaves_like 'command with multiple arguments' do
let(:original_msg) { %(hello\n/labels ~foo ~"bar baz" label\nworld) }
let(:final_msg) { "hello\nworld" }
end
end
context 'in the middle of a line' do
it 'does not extract command' do
msg = %(hello\nworld /labels ~foo ~"bar baz" label)
msg, commands = extractor.extract_commands(msg)
expect(commands).to be_empty
expect(msg).to eq %(hello\nworld /labels ~foo ~"bar baz" label)
end
end
context 'at the end of content' do
it_behaves_like 'command with multiple arguments' do
let(:original_msg) { %(hello\n/labels ~foo ~"bar baz" label) }
let(:final_msg) { "hello" }
end
end
context 'when argument is not separated with a space' do
it 'does not extract command' do
msg = %(hello\n/labels~foo ~"bar baz" label\nworld)
msg, commands = extractor.extract_commands(msg)
expect(commands).to be_empty
expect(msg).to eq %(hello\n/labels~foo ~"bar baz" label\nworld)
end
end
end
it 'extracts command with multiple arguments and various prefixes' do
msg = %(hello\n/power @user.name %9.10 ~"bar baz.2"\nworld)
msg, commands = extractor.extract_commands(msg)
expect(commands).to eq [['power', '@user.name %9.10 ~"bar baz.2"']]
expect(msg).to eq "hello\nworld"
end
it 'extracts multiple commands' do
msg = %(hello\n/power @user.name %9.10 ~"bar baz.2" label\nworld\n/reopen)
msg, commands = extractor.extract_commands(msg)
expect(commands).to eq [['power', '@user.name %9.10 ~"bar baz.2" label'], ['reopen']]
expect(msg).to eq "hello\nworld"
end
it 'does not alter original content if no command is found' do
msg = 'Fixes #123'
msg, commands = extractor.extract_commands(msg)
expect(commands).to be_empty
expect(msg).to eq 'Fixes #123'
end
it 'does not extract commands inside a blockcode' do
msg = "Hello\r\n```\r\nThis is some text\r\n/close\r\n/assign @user\r\n```\r\n\r\nWorld"
expected = msg.delete("\r")
msg, commands = extractor.extract_commands(msg)
expect(commands).to be_empty
expect(msg).to eq expected
end
it 'does not extract commands inside a blockquote' do
msg = "Hello\r\n>>>\r\nThis is some text\r\n/close\r\n/assign @user\r\n>>>\r\n\r\nWorld"
expected = msg.delete("\r")
msg, commands = extractor.extract_commands(msg)
expect(commands).to be_empty
expect(msg).to eq expected
end
it 'does not extract commands inside a HTML tag' do
msg = "Hello\r\n<div>\r\nThis is some text\r\n/close\r\n/assign @user\r\n</div>\r\n\r\nWorld"
expected = msg.delete("\r")
msg, commands = extractor.extract_commands(msg)
expect(commands).to be_empty
expect(msg).to eq expected
end
end
end
...@@ -407,4 +407,27 @@ describe API::API, api: true do ...@@ -407,4 +407,27 @@ describe API::API, api: true do
end end
end end
end end
describe 'POST /projects/:id/builds/:build_id/play' do
before do
post api("/projects/#{project.id}/builds/#{build.id}/play", user)
end
context 'on an playable build' do
let(:build) { create(:ci_build, :manual, project: project, pipeline: pipeline) }
it 'plays the build' do
expect(response).to have_http_status 200
expect(json_response['user']['id']).to eq(user.id)
expect(json_response['id']).to eq(build.id)
end
end
context 'on a non-playable build' do
it 'returns a status code 400, Bad Request' do
expect(response).to have_http_status 400
expect(response.body).to match("Unplayable Build")
end
end
end
end end
require 'spec_helper'
describe API::API, api: true do
include ApiHelpers
let(:user) { create(:user) }
let(:non_member) { create(:user) }
let(:project) { deployment.environment.project }
let!(:deployment) { create(:deployment) }
before do
project.team << [user, :master]
end
describe 'GET /projects/:id/deployments' do
context 'as member of the project' do
it_behaves_like 'a paginated resources' do
let(:request) { get api("/projects/#{project.id}/deployments", user) }
end
it 'returns projects deployments' do
get api("/projects/#{project.id}/deployments", user)
expect(response).to have_http_status(200)
expect(json_response).to be_an Array
expect(json_response.size).to eq(1)
expect(json_response.first['iid']).to eq(deployment.iid)
expect(json_response.first['sha']).to match /\A\h{40}\z/
end
end
context 'as non member' do
it 'returns a 404 status code' do
get api("/projects/#{project.id}/deployments", non_member)
expect(response).to have_http_status(404)
end
end
end
describe 'GET /projects/:id/deployments/:deployment_id' do
context 'as a member of the project' do
it 'returns the projects deployment' do
get api("/projects/#{project.id}/deployments/#{deployment.id}", user)
expect(response).to have_http_status(200)
expect(json_response['sha']).to match /\A\h{40}\z/
expect(json_response['id']).to eq(deployment.id)
end
end
context 'as non member' do
it 'returns a 404 status code' do
get api("/projects/#{project.id}/deployments/#{deployment.id}", non_member)
expect(response).to have_http_status(404)
end
end
end
end
...@@ -26,6 +26,7 @@ describe API::API, api: true do ...@@ -26,6 +26,7 @@ describe API::API, api: true do
expect(json_response.size).to eq(1) expect(json_response.size).to eq(1)
expect(json_response.first['name']).to eq(environment.name) expect(json_response.first['name']).to eq(environment.name)
expect(json_response.first['external_url']).to eq(environment.external_url) expect(json_response.first['external_url']).to eq(environment.external_url)
expect(json_response.first['project']['id']).to eq(project.id)
end end
end end
......
require 'spec_helper'
describe API::API, api: true do
include ApiHelpers
let(:user) { create(:user) }
let(:non_member) { create(:user) }
let(:project) { create(:project, creator_id: user.id) }
let!(:pipeline) do
create(:ci_empty_pipeline, project: project, sha: project.commit.id,
ref: project.default_branch)
end
before { project.team << [user, :master] }
describe 'GET /projects/:id/pipelines ' do
it_behaves_like 'a paginated resources' do
let(:request) { get api("/projects/#{project.id}/pipelines", user) }
end
context 'authorized user' do
it 'returns project pipelines' do
get api("/projects/#{project.id}/pipelines", user)
expect(response).to have_http_status(200)
expect(json_response).to be_an Array
expect(json_response.first['sha']).to match /\A\h{40}\z/
expect(json_response.first['id']).to eq pipeline.id
end
end
context 'unauthorized user' do
it 'does not return project pipelines' do
get api("/projects/#{project.id}/pipelines", non_member)
expect(response).to have_http_status(404)
expect(json_response['message']).to eq '404 Project Not Found'
expect(json_response).not_to be_an Array
end
end
end
describe 'GET /projects/:id/pipelines/:pipeline_id' do
context 'authorized user' do
it 'returns project pipelines' do
get api("/projects/#{project.id}/pipelines/#{pipeline.id}", user)
expect(response).to have_http_status(200)
expect(json_response['sha']).to match /\A\h{40}\z/
end
it 'returns 404 when it does not exist' do
get api("/projects/#{project.id}/pipelines/123456", user)
expect(response).to have_http_status(404)
expect(json_response['message']).to eq '404 Not found'
expect(json_response['id']).to be nil
end
end
context 'unauthorized user' do
it 'should not return a project pipeline' do
get api("/projects/#{project.id}/pipelines/#{pipeline.id}", non_member)
expect(response).to have_http_status(404)
expect(json_response['message']).to eq '404 Project Not Found'
expect(json_response['id']).to be nil
end
end
end
describe 'POST /projects/:id/pipelines/:pipeline_id/retry' do
context 'authorized user' do
let!(:pipeline) do
create(:ci_pipeline, project: project, sha: project.commit.id,
ref: project.default_branch)
end
let!(:build) { create(:ci_build, :failed, pipeline: pipeline) }
it 'retries failed builds' do
expect do
post api("/projects/#{project.id}/pipelines/#{pipeline.id}/retry", user)
end.to change { pipeline.builds.count }.from(1).to(2)
expect(response).to have_http_status(201)
expect(build.reload.retried?).to be true
end
end
context 'unauthorized user' do
it 'should not return a project pipeline' do
post api("/projects/#{project.id}/pipelines/#{pipeline.id}/retry", non_member)
expect(response).to have_http_status(404)
expect(json_response['message']).to eq '404 Project Not Found'
expect(json_response['id']).to be nil
end
end
end
describe 'POST /projects/:id/pipelines/:pipeline_id/cancel' do
let!(:pipeline) do
create(:ci_empty_pipeline, project: project, sha: project.commit.id,
ref: project.default_branch)
end
let!(:build) { create(:ci_build, :running, pipeline: pipeline) }
context 'authorized user' do
it 'retries failed builds' do
post api("/projects/#{project.id}/pipelines/#{pipeline.id}/cancel", user)
expect(response).to have_http_status(200)
expect(json_response['status']).to eq('canceled')
end
end
context 'user without proper access rights' do
let!(:reporter) { create(:user) }
before { project.team << [reporter, :reporter] }
it 'rejects the action' do
post api("/projects/#{project.id}/pipelines/#{pipeline.id}/cancel", reporter)
expect(response).to have_http_status(403)
expect(pipeline.reload.status).to eq('pending')
end
end
end
end
...@@ -3,6 +3,7 @@ require 'spec_helper' ...@@ -3,6 +3,7 @@ require 'spec_helper'
describe Issues::CloseService, services: true do describe Issues::CloseService, services: true do
let(:user) { create(:user) } let(:user) { create(:user) }
let(:user2) { create(:user) } let(:user2) { create(:user) }
let(:guest) { create(:user) }
let(:issue) { create(:issue, assignee: user2) } let(:issue) { create(:issue, assignee: user2) }
let(:project) { issue.project } let(:project) { issue.project }
let!(:todo) { create(:todo, :assigned, user: user, project: project, target: issue, author: user2) } let!(:todo) { create(:todo, :assigned, user: user, project: project, target: issue, author: user2) }
...@@ -10,13 +11,14 @@ describe Issues::CloseService, services: true do ...@@ -10,13 +11,14 @@ describe Issues::CloseService, services: true do
before do before do
project.team << [user, :master] project.team << [user, :master]
project.team << [user2, :developer] project.team << [user2, :developer]
project.team << [guest, :guest]
end end
describe '#execute' do describe '#execute' do
context "valid params" do context "valid params" do
before do before do
perform_enqueued_jobs do perform_enqueued_jobs do
@issue = Issues::CloseService.new(project, user, {}).execute(issue) @issue = described_class.new(project, user, {}).execute(issue)
end end
end end
...@@ -39,10 +41,22 @@ describe Issues::CloseService, services: true do ...@@ -39,10 +41,22 @@ describe Issues::CloseService, services: true do
end end
end end
context 'current user is not authorized to close issue' do
before do
perform_enqueued_jobs do
@issue = described_class.new(project, guest).execute(issue)
end
end
it 'does not close the issue' do
expect(@issue).to be_open
end
end
context "external issue tracker" do context "external issue tracker" do
before do before do
allow(project).to receive(:default_issues_tracker?).and_return(false) allow(project).to receive(:default_issues_tracker?).and_return(false)
@issue = Issues::CloseService.new(project, user, {}).execute(issue) @issue = described_class.new(project, user, {}).execute(issue)
end end
it { expect(@issue).to be_valid } it { expect(@issue).to be_valid }
......
...@@ -73,5 +73,7 @@ describe Issues::CreateService, services: true do ...@@ -73,5 +73,7 @@ describe Issues::CreateService, services: true do
end end
end end
end end
it_behaves_like 'new issuable record that supports slash commands'
end end
end end
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment