summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorDouwe Maan <douwe@selenight.nl>2016-08-18 16:43:26 -0500
committerDouwe Maan <douwe@selenight.nl>2016-08-18 16:43:26 -0500
commit8b05e8c2d10f78467db8bec140950104eba49e92 (patch)
tree3933fdeb2089a448c82aa6c75b3087bcbca665f9
parentb5d0346a99401ec138ee411660b5c7ee28421f84 (diff)
parentd97c83096a70ed102f993d4c484aff9c786ca831 (diff)
downloadgitlab-ce-8b05e8c2d10f78467db8bec140950104eba49e92.tar.gz
Merge branch 'master' into diff-line-comment-vuejs
# Conflicts: # db/schema.rb
-rw-r--r--CHANGELOG13
-rw-r--r--Gemfile4
-rw-r--r--Gemfile.lock8
-rw-r--r--app/assets/javascripts/abuse_reports.js.es639
-rw-r--r--app/assets/javascripts/application.js2
-rw-r--r--app/assets/javascripts/awards_handler.js54
-rw-r--r--app/assets/javascripts/boards/vue_resource_interceptor.js.es68
-rw-r--r--app/assets/javascripts/dispatcher.js3
-rw-r--r--app/assets/javascripts/lib/utils/text_utility.js5
-rw-r--r--app/assets/stylesheets/mailers/repository_push_email.scss5
-rw-r--r--app/assets/stylesheets/pages/admin.scss42
-rw-r--r--app/assets/stylesheets/pages/profile.scss6
-rw-r--r--app/controllers/dashboard/todos_controller.rb6
-rw-r--r--app/controllers/profiles/two_factor_auths_controller.rb12
-rw-r--r--app/controllers/profiles/u2f_registrations_controller.rb7
-rw-r--r--app/helpers/blob_helper.rb9
-rw-r--r--app/helpers/issues_helper.rb14
-rw-r--r--app/models/u2f_registration.rb7
-rw-r--r--app/services/todo_service.rb6
-rw-r--r--app/views/admin/abuse_reports/_abuse_report.html.haml17
-rw-r--r--app/views/admin/abuse_reports/index.html.haml33
-rw-r--r--app/views/notify/repository_push_email.html.haml2
-rw-r--r--app/views/profiles/two_factor_auths/show.html.haml31
-rw-r--r--app/views/projects/diffs/_file.html.haml7
-rw-r--r--app/views/projects/diffs/_line.html.haml7
-rw-r--r--app/views/u2f/_register.html.haml13
-rw-r--r--config/routes.rb2
-rw-r--r--db/migrate/20160816161312_add_column_name_to_u2f_registrations.rb29
-rw-r--r--db/schema.rb1
-rw-r--r--doc/api/README.md1
-rw-r--r--doc/api/builds.md46
-rw-r--r--doc/api/deployments.md218
-rw-r--r--doc/api/pipelines.md207
-rw-r--r--doc/ci/yaml/README.md2
-rw-r--r--doc/legal/corporate_contributor_license_agreement.md4
-rw-r--r--features/steps/project/issues/award_emoji.rb2
-rw-r--r--features/steps/project/issues/issues.rb2
-rw-r--r--lib/api/api.rb2
-rw-r--r--lib/api/builds.rb21
-rw-r--r--lib/api/deployments.rb40
-rw-r--r--lib/api/entities.rb20
-rw-r--r--lib/api/pipelines.rb74
-rw-r--r--spec/factories/ci/pipelines.rb18
-rw-r--r--spec/features/boards/boards_spec.rb26
-rw-r--r--spec/features/u2f_spec.rb36
-rw-r--r--spec/helpers/blob_helper_spec.rb26
-rw-r--r--spec/helpers/issues_helper_spec.rb26
-rw-r--r--spec/javascripts/abuse_reports_spec.js.es641
-rw-r--r--spec/javascripts/awards_handler_spec.js46
-rw-r--r--spec/javascripts/fixtures/abuse_reports.html.haml16
-rw-r--r--spec/requests/api/builds_spec.rb23
-rw-r--r--spec/requests/api/deployments_spec.rb60
-rw-r--r--spec/requests/api/environments_spec.rb1
-rw-r--r--spec/requests/api/pipelines_spec.rb133
-rw-r--r--spec/services/todo_service_spec.rb30
-rw-r--r--spec/support/fake_u2f_device.rb5
56 files changed, 1379 insertions, 139 deletions
diff --git a/CHANGELOG b/CHANGELOG
index 8185982c02f..e14f66af6fd 100644
--- a/CHANGELOG
+++ b/CHANGELOG
@@ -11,6 +11,7 @@ v 8.11.0 (unreleased)
- Rename `markdown_preview` routes to `preview_markdown`. (Christopher Bartz)
- Update to Ruby 2.3.1. !4948
- Add Issues Board !5548
+ - Allow resolving merge conflicts in the UI !5479
- 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)
- Convert switch icon into icon font (ClemMakesApps)
@@ -20,6 +21,7 @@ v 8.11.0 (unreleased)
- 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
- 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)
- Fix CI status icon link underline (ClemMakesApps)
- The Repository class is now instrumented
@@ -29,6 +31,8 @@ v 8.11.0 (unreleased)
- Cache highlighted diff lines for merge requests
- Pre-create all builds for a Pipeline when the new Pipeline is created !5295
- Allow merge request diff notes and discussions to be explicitly marked as resolved
+ - 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'
- Show member roles to all users on members page
- Project.visible_to_user is instrumented again
@@ -52,8 +56,10 @@ v 8.11.0 (unreleased)
- Show deployment status on merge requests with external URLs
- Clean up unused routes (Josef Strzibny)
- 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)
- Optimize generating of cache keys for issues and notes
+ - Fix repository push email formatting in Outlook
- Improve performance of syntax highlighting Markdown code blocks
- 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
@@ -65,6 +71,7 @@ v 8.11.0 (unreleased)
- 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
- Store all DB secrets in secrets.yml, under descriptive names !5274
+ - Fix syntax highlighting in file editor
- Nokogiri's various parsing methods are now instrumented
- Add archived badge to project list !5798
- Add simple identifier to public SSH keys (muteor)
@@ -82,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)
- Load project invited groups and members eagerly in `ProjectTeam#fetch_members`
- 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
- Rewrite description of a blocked user in admin settings. (Elias Werberich)
- Make branches sortable without push permission !5462 (winniehell)
@@ -104,6 +113,7 @@ v 8.11.0 (unreleased)
- Add commit stats in commit api. !5517 (dixpac)
- Add CI configuration button on project page
- 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)
- The performance of the project dropdown used for moving issues has been improved
- Fix skip_repo parameter being ignored when destroying a namespace
@@ -127,10 +137,13 @@ v 8.11.0 (unreleased)
- Sort folders with submodules in Files view !5521
- 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 pipelines tab to merge requests
- Fix a memory leak caused by Banzai::Filter::SanitizationFilter
- 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
- 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
- Upgrade Rails to 4.2.7.1 for security fixes. !5781
diff --git a/Gemfile b/Gemfile
index a6fcc3575ff..44d2fe557f1 100644
--- a/Gemfile
+++ b/Gemfile
@@ -53,7 +53,7 @@ gem 'browser', '~> 2.2'
# Extracting information from a git repository
# Provide access to Gitlab::Git library
-gem 'gitlab_git', '~> 10.4.5'
+gem 'gitlab_git', '~> 10.4.7'
# LDAP Auth
# GitLab fork with several improvements to original library. For full list of changes
@@ -201,7 +201,7 @@ gem 'licensee', '~> 8.0.0'
gem 'rack-attack', '~> 4.3.1'
# Ace editor
-gem 'ace-rails-ap', '~> 4.0.2'
+gem 'ace-rails-ap', '~> 4.1.0'
# Keyboard shortcuts
gem 'mousetrap-rails', '~> 1.4.6'
diff --git a/Gemfile.lock b/Gemfile.lock
index 58c84c47575..ff95fb4ceed 100644
--- a/Gemfile.lock
+++ b/Gemfile.lock
@@ -2,7 +2,7 @@ GEM
remote: https://rubygems.org/
specs:
RedCloth (4.3.2)
- ace-rails-ap (4.0.2)
+ ace-rails-ap (4.1.0)
actionmailer (4.2.7.1)
actionpack (= 4.2.7.1)
actionview (= 4.2.7.1)
@@ -278,7 +278,7 @@ GEM
diff-lcs (~> 1.1)
mime-types (>= 1.16, < 3)
posix-spawn (~> 0.3)
- gitlab_git (10.4.5)
+ gitlab_git (10.4.7)
activesupport (~> 4.0)
charlock_holmes (~> 0.7.3)
github-linguist (~> 4.7.0)
@@ -798,7 +798,7 @@ PLATFORMS
DEPENDENCIES
RedCloth (~> 4.3.2)
- ace-rails-ap (~> 4.0.2)
+ ace-rails-ap (~> 4.1.0)
activerecord-session_store (~> 1.0.0)
acts-as-taggable-on (~> 3.4)
addressable (~> 2.3.8)
@@ -857,7 +857,7 @@ DEPENDENCIES
github-linguist (~> 4.7.0)
github-markup (~> 1.4)
gitlab-flowdock-git-hook (~> 1.0.1)
- gitlab_git (~> 10.4.5)
+ gitlab_git (~> 10.4.7)
gitlab_meta (= 7.0)
gitlab_omniauth-ldap (~> 1.2.1)
gollum-lib (~> 4.2)
diff --git a/app/assets/javascripts/abuse_reports.js.es6 b/app/assets/javascripts/abuse_reports.js.es6
new file mode 100644
index 00000000000..748084b0307
--- /dev/null
+++ b/app/assets/javascripts/abuse_reports.js.es6
@@ -0,0 +1,39 @@
+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);
diff --git a/app/assets/javascripts/application.js b/app/assets/javascripts/application.js
index 4b8e687f3d4..a122fa2d637 100644
--- a/app/assets/javascripts/application.js
+++ b/app/assets/javascripts/application.js
@@ -26,7 +26,7 @@
/*= require bootstrap/tooltip */
/*= require bootstrap/popover */
/*= require select2 */
-/*= require ace/ace */
+/*= require ace-rails-ap */
/*= require ace/ext-searchbox */
/*= require underscore */
/*= require dropzone */
diff --git a/app/assets/javascripts/awards_handler.js b/app/assets/javascripts/awards_handler.js
index 2c5b83e4f1e..aee1c29eee3 100644
--- a/app/assets/javascripts/awards_handler.js
+++ b/app/assets/javascripts/awards_handler.js
@@ -1,5 +1,6 @@
(function() {
this.AwardsHandler = (function() {
+ const FROM_SENTENCE_REGEX = /(?:, and | and |, )/; //For separating lists produced by ruby's Array#toSentence
function AwardsHandler() {
this.aliases = gl.emojiAliases();
$(document).off('click', '.js-add-award').on('click', '.js-add-award', (function(_this) {
@@ -130,7 +131,7 @@
counter = $emojiButton.find('.js-counter');
counter.text(parseInt(counter.text()) + 1);
$emojiButton.addClass('active');
- this.addMeToUserList(votesBlock, emoji);
+ this.addYouToUserList(votesBlock, emoji);
return this.animateEmoji($emojiButton);
}
} else {
@@ -176,11 +177,11 @@
counterNumber = parseInt(counter.text(), 10);
if (counterNumber > 1) {
counter.text(counterNumber - 1);
- this.removeMeFromUserList($emojiButton, emoji);
+ this.removeYouFromUserList($emojiButton, emoji);
} else if (emoji === 'thumbsup' || emoji === 'thumbsdown') {
$emojiButton.tooltip('destroy');
counter.text('0');
- this.removeMeFromUserList($emojiButton, emoji);
+ this.removeYouFromUserList($emojiButton, emoji);
if ($emojiButton.parents('.note').length) {
this.removeEmoji($emojiButton);
}
@@ -204,43 +205,48 @@
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;
awardBlock = $emojiButton;
originalTitle = this.getAwardTooltip(awardBlock);
- authors = originalTitle.split(', ');
- authors.splice(authors.indexOf('me'), 1);
- newAuthors = authors.join(', ');
- awardBlock.closest('.js-emoji-btn').removeData('original-title').attr('data-original-title', newAuthors);
- return this.resetTooltip(awardBlock);
+ authors = originalTitle.split(FROM_SENTENCE_REGEX);
+ authors.splice(authors.indexOf('You'), 1);
+ return awardBlock
+ .closest('.js-emoji-btn')
+ .removeData('title')
+ .removeAttr('data-title')
+ .removeAttr('data-original-title')
+ .attr('title', this.toSentence(authors))
+ .tooltip('fixTitle');
};
- AwardsHandler.prototype.addMeToUserList = function(votesBlock, emoji) {
+ AwardsHandler.prototype.addYouToUserList = function(votesBlock, emoji) {
var awardBlock, origTitle, users;
awardBlock = this.findEmojiIcon(votesBlock, emoji).parent();
origTitle = this.getAwardTooltip(awardBlock);
users = [];
if (origTitle) {
- users = origTitle.trim().split(', ');
+ users = origTitle.trim().split(FROM_SENTENCE_REGEX);
}
- users.push('me');
- awardBlock.attr('title', users.join(', '));
- return this.resetTooltip(awardBlock);
- };
-
- AwardsHandler.prototype.resetTooltip = function(award) {
- var cb;
- award.tooltip('destroy');
- cb = function() {
- return award.tooltip();
- };
- return setTimeout(cb, 200);
+ users.unshift('You');
+ return awardBlock
+ .attr('title', this.toSentence(users))
+ .tooltip('fixTitle');
};
AwardsHandler.prototype.createEmoji_ = function(votesBlock, emoji) {
var $emojiButton, buttonHtml, emojiCssClass;
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.insertBefore(votesBlock.find('.js-award-holder')).find('.emoji-icon').data('emoji', emoji);
this.animateEmoji($emojiButton);
diff --git a/app/assets/javascripts/boards/vue_resource_interceptor.js.es6 b/app/assets/javascripts/boards/vue_resource_interceptor.js.es6
index 66f645a4b61..f9f9f7999d4 100644
--- a/app/assets/javascripts/boards/vue_resource_interceptor.js.es6
+++ b/app/assets/javascripts/boards/vue_resource_interceptor.js.es6
@@ -1,8 +1,10 @@
Vue.http.interceptors.push((request, next) => {
Vue.activeResources = Vue.activeResources ? Vue.activeResources + 1 : 1;
- setTimeout(() => {
- Vue.activeResources--;
- }, 500);
+ Vue.nextTick(() => {
+ setTimeout(() => {
+ Vue.activeResources--;
+ }, 500);
+ });
next();
});
diff --git a/app/assets/javascripts/dispatcher.js b/app/assets/javascripts/dispatcher.js
index 1163edd8547..74c4ab563f9 100644
--- a/app/assets/javascripts/dispatcher.js
+++ b/app/assets/javascripts/dispatcher.js
@@ -196,6 +196,9 @@
case 'edit':
new Labels();
}
+ case 'abuse_reports':
+ new gl.AbuseReports();
+ break;
}
break;
case 'dashboard':
diff --git a/app/assets/javascripts/lib/utils/text_utility.js b/app/assets/javascripts/lib/utils/text_utility.js
index 130479642f3..b6636de5767 100644
--- a/app/assets/javascripts/lib/utils/text_utility.js
+++ b/app/assets/javascripts/lib/utils/text_utility.js
@@ -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 gl.text.removeListeners = function(form) {
+ gl.text.removeListeners = function(form) {
return $('.js-md', form).off();
};
+ return gl.text.truncate = function(string, maxLength) {
+ return string.substr(0, (maxLength - 3)) + '...';
+ };
})(window);
}).call(this);
diff --git a/app/assets/stylesheets/mailers/repository_push_email.scss b/app/assets/stylesheets/mailers/repository_push_email.scss
index 33aedf1f7c1..5bfe9bcb443 100644
--- a/app/assets/stylesheets/mailers/repository_push_email.scss
+++ b/app/assets/stylesheets/mailers/repository_push_email.scss
@@ -45,7 +45,6 @@
.line_content {
padding-left: 0.5em;
padding-right: 0.5em;
- white-space: pre;
&.old {
background-color: $line-removed;
@@ -71,6 +70,10 @@
}
}
+pre {
+ margin: 0;
+}
+
span.highlight_word {
background-color: #fafe3d !important;
}
diff --git a/app/assets/stylesheets/pages/admin.scss b/app/assets/stylesheets/pages/admin.scss
index 5607239d92d..c9cdfdcd29c 100644
--- a/app/assets/stylesheets/pages/admin.scss
+++ b/app/assets/stylesheets/pages/admin.scss
@@ -72,7 +72,6 @@
margin-bottom: 20px;
}
-
// Users List
.users-list {
@@ -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;
+ }
+ }
+}
diff --git a/app/assets/stylesheets/pages/profile.scss b/app/assets/stylesheets/pages/profile.scss
index 46371ec6871..6f58203f49c 100644
--- a/app/assets/stylesheets/pages/profile.scss
+++ b/app/assets/stylesheets/pages/profile.scss
@@ -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
diff --git a/app/controllers/dashboard/todos_controller.rb b/app/controllers/dashboard/todos_controller.rb
index 1243bb96d4d..c8390af3b36 100644
--- a/app/controllers/dashboard/todos_controller.rb
+++ b/app/controllers/dashboard/todos_controller.rb
@@ -6,7 +6,7 @@ class Dashboard::TodosController < Dashboard::ApplicationController
end
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|
format.html { redirect_to dashboard_todos_path, notice: 'Todo was successfully marked as done.' }
@@ -27,10 +27,6 @@ class Dashboard::TodosController < Dashboard::ApplicationController
private
- def todo
- @todo ||= find_todos.find(params[:id])
- end
-
def find_todos
@todos ||= TodosFinder.new(current_user, params).execute
end
diff --git a/app/controllers/profiles/two_factor_auths_controller.rb b/app/controllers/profiles/two_factor_auths_controller.rb
index e37e9e136db..9eb75bb3891 100644
--- a/app/controllers/profiles/two_factor_auths_controller.rb
+++ b/app/controllers/profiles/two_factor_auths_controller.rb
@@ -43,11 +43,11 @@ class Profiles::TwoFactorAuthsController < Profiles::ApplicationController
# A U2F (universal 2nd factor) device's information is stored after successful
# registration, which is then used while 2FA authentication is taking place.
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?
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
@qr_code = build_qr_code
setup_u2f_registration
@@ -91,15 +91,19 @@ class Profiles::TwoFactorAuthsController < Profiles::ApplicationController
# Actual communication is performed using a Javascript API
def setup_u2f_registration
@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)
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)
gon.push(u2f: { challenges: session[:challenges], app_id: u2f_app_id,
register_requests: registration_requests,
sign_requests: sign_requests })
end
+
+ def u2f_registration_params
+ params.require(:u2f_registration).permit(:device_response, :name)
+ end
end
diff --git a/app/controllers/profiles/u2f_registrations_controller.rb b/app/controllers/profiles/u2f_registrations_controller.rb
new file mode 100644
index 00000000000..c02fe85c3cc
--- /dev/null
+++ b/app/controllers/profiles/u2f_registrations_controller.rb
@@ -0,0 +1,7 @@
+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
diff --git a/app/helpers/blob_helper.rb b/app/helpers/blob_helper.rb
index 1cb5d847626..9ea03720c1e 100644
--- a/app/helpers/blob_helper.rb
+++ b/app/helpers/blob_helper.rb
@@ -11,17 +11,14 @@ module BlobHelper
def edit_blob_link(project = @project, ref = @ref, path = @path, options = {})
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
- 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,
tree_join(ref, path),
- link_opts)
+ options[:link_opts])
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' }
diff --git a/app/helpers/issues_helper.rb b/app/helpers/issues_helper.rb
index 2e82b44437b..8b212b0327a 100644
--- a/app/helpers/issues_helper.rb
+++ b/app/helpers/issues_helper.rb
@@ -114,9 +114,17 @@ module IssuesHelper
end
def award_user_list(awards, current_user)
- awards.map do |award|
- award.user == current_user ? 'me' : award.user.name
- end.join(', ')
+ names = awards.map do |award|
+ award.user == current_user ? 'You' : award.user.name
+ 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
def award_active_class(awards, current_user)
diff --git a/app/models/u2f_registration.rb b/app/models/u2f_registration.rb
index 00b19686d48..808acec098f 100644
--- a/app/models/u2f_registration.rb
+++ b/app/models/u2f_registration.rb
@@ -3,18 +3,19 @@
class U2fRegistration < ActiveRecord::Base
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)
registration = self.new
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.update(certificate: registration_data.certificate,
key_handle: registration_data.key_handle,
public_key: registration_data.public_key,
counter: registration_data.counter,
- user: user)
+ user: user,
+ name: params[:name])
rescue JSON::ParserError, NoMethodError, ArgumentError
registration.errors.add(:base, 'Your U2F device did not send a valid JSON response.')
rescue U2F::Error => e
diff --git a/app/services/todo_service.rb b/app/services/todo_service.rb
index eb833dd82ac..daf4339cb48 100644
--- a/app/services/todo_service.rb
+++ b/app/services/todo_service.rb
@@ -142,7 +142,11 @@ class TodoService
# When user marks some todos as done
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)
current_user.update_todos_count_cache
diff --git a/app/views/admin/abuse_reports/_abuse_report.html.haml b/app/views/admin/abuse_reports/_abuse_report.html.haml
index dd2e7ebd030..56bf6194914 100644
--- a/app/views/admin/abuse_reports/_abuse_report.html.haml
+++ b/app/views/admin/abuse_reports/_abuse_report.html.haml
@@ -1,6 +1,8 @@
- reporter = abuse_report.reporter
- user = abuse_report.user
%tr
+ %th.visible-xs-block.visible-sm-block
+ %strong User
%td
- if user
= link_to user.name, user
@@ -9,6 +11,7 @@
- else
(removed)
%td
+ %strong.subheading.visible-xs-block.visible-sm-block Reported by
- if reporter
= link_to reporter.name, reporter
- else
@@ -16,16 +19,16 @@
.light.small
= time_ago_with_tooltip(abuse_report.created_at)
%td
- = markdown(abuse_report.message.squish!, pipeline: :single_line, author: reporter)
+ %strong.subheading.visible-xs-block.visible-sm-block Message
+ .message
+ = markdown(abuse_report.message.squish!, pipeline: :single_line, author: reporter)
%td
- if user
= 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"
-
- %td
+ 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"
- 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
- .btn.btn-xs.disabled
+ .btn.btn-sm.disabled.btn-block
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"
diff --git a/app/views/admin/abuse_reports/index.html.haml b/app/views/admin/abuse_reports/index.html.haml
index bc4a9cedb2c..7bbc75db9ff 100644
--- a/app/views/admin/abuse_reports/index.html.haml
+++ b/app/views/admin/abuse_reports/index.html.haml
@@ -1,17 +1,20 @@
-- page_title "Abuse Reports"
+- page_title 'Abuse Reports'
%h3.page-title Abuse Reports
%hr
-- if @abuse_reports.present?
- .table-holder
- %table.table
- %thead
- %tr
- %th User
- %th Reported by
- %th Message
- %th Primary action
- %th
- = render @abuse_reports
- = paginate @abuse_reports
-- else
- %h4 There are no abuse reports
+.abuse-reports
+ - if @abuse_reports.present?
+ .table-holder
+ %table.table
+ %thead.hidden-sm.hidden-xs
+ %tr
+ %th User
+ %th Reported by
+ %th.wide Message
+ %th Action
+ = render @abuse_reports
+ - else
+ .no-reports
+ %span.pull-left
+ There are no abuse reports!
+ .pull-left
+ = emoji_icon 'tada'
diff --git a/app/views/notify/repository_push_email.html.haml b/app/views/notify/repository_push_email.html.haml
index c161ecc3463..4dc39a72225 100644
--- a/app/views/notify/repository_push_email.html.haml
+++ b/app/views/notify/repository_push_email.html.haml
@@ -76,7 +76,7 @@
- if blob && blob.respond_to?(:text?) && blob_text_viewable?(blob)
%table.code.white
- 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
No preview for this file type
%br
diff --git a/app/views/profiles/two_factor_auths/show.html.haml b/app/views/profiles/two_factor_auths/show.html.haml
index 366f1fed35b..03ac739ade5 100644
--- a/app/views/profiles/two_factor_auths/show.html.haml
+++ b/app/views/profiles/two_factor_auths/show.html.haml
@@ -60,13 +60,38 @@
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.
.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?
= form_errors(@u2f_registration)
= 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?
: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>";
diff --git a/app/views/projects/diffs/_file.html.haml b/app/views/projects/diffs/_file.html.haml
index 8fbd89100ca..ad2eb3e504f 100644
--- a/app/views/projects/diffs/_file.html.haml
+++ b/app/views/projects/diffs/_file.html.haml
@@ -10,10 +10,9 @@
\
- if editable_diff?(diff_file)
- = edit_blob_link(@merge_request.source_project,
- @merge_request.source_branch, diff_file.new_path,
- from_merge_request_id: @merge_request.id,
- skip_visible_check: true)
+ - link_opts = @merge_request.id ? { from_merge_request_id: @merge_request.id } : {}
+ = edit_blob_link(@merge_request.source_project, @merge_request.source_branch, diff_file.new_path,
+ blob: blob, link_opts: link_opts)
= view_file_btn(diff_commit.id, diff_file.new_path, project)
diff --git a/app/views/projects/diffs/_line.html.haml b/app/views/projects/diffs/_line.html.haml
index 2d6a370b848..891b2bd9802 100644
--- a/app/views/projects/diffs/_line.html.haml
+++ b/app/views/projects/diffs/_line.html.haml
@@ -1,3 +1,4 @@
+- email = local_assigns.fetch(:email, false)
- plain = local_assigns.fetch(:plain, false)
- type = line.type
- line_code = diff_file.line_code(line) unless plain
@@ -22,4 +23,8 @@
= link_text
- else
%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)
diff --git a/app/views/u2f/_register.html.haml b/app/views/u2f/_register.html.haml
index cbb8dfb7829..8f7b42eb351 100644
--- a/app/views/u2f/_register.html.haml
+++ b/app/views/u2f/_register.html.haml
@@ -28,10 +28,15 @@
%script#js-register-u2f-registered{ type: "text/template" }
%div.row.append-bottom-10
- %p Your device was successfully set up! Click this button to register with the GitLab server.
- = 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"
- = submit_tag "Register U2F Device", class: "btn btn-success"
+ .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
+ .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"
:javascript
var u2fRegister = new U2FRegister($("#js-register-u2f"), gon.u2f);
diff --git a/config/routes.rb b/config/routes.rb
index 624e07b0947..66f77aee06a 100644
--- a/config/routes.rb
+++ b/config/routes.rb
@@ -375,6 +375,8 @@ Rails.application.routes.draw do
patch :skip
end
end
+
+ resources :u2f_registrations, only: [:destroy]
end
end
diff --git a/db/migrate/20160816161312_add_column_name_to_u2f_registrations.rb b/db/migrate/20160816161312_add_column_name_to_u2f_registrations.rb
new file mode 100644
index 00000000000..7152bd04331
--- /dev/null
+++ b/db/migrate/20160816161312_add_column_name_to_u2f_registrations.rb
@@ -0,0 +1,29 @@
+# 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
diff --git a/db/schema.rb b/db/schema.rb
index 68651a45fa6..82d4590f6b5 100644
--- a/db/schema.rb
+++ b/db/schema.rb
@@ -1020,6 +1020,7 @@ ActiveRecord::Schema.define(version: 20160817154936) do
t.integer "user_id"
t.datetime "created_at", null: false
t.datetime "updated_at", null: false
+ t.string "name"
end
add_index "u2f_registrations", ["key_handle"], name: "index_u2f_registrations_on_key_handle", using: :btree
diff --git a/doc/api/README.md b/doc/api/README.md
index f3117815c7c..3e79cce0120 100644
--- a/doc/api/README.md
+++ b/doc/api/README.md
@@ -26,6 +26,7 @@ following locations:
- [Open source license templates](licenses.md)
- [Namespaces](namespaces.md)
- [Notes](notes.md) (comments)
+- [Pipelines](pipelines.md)
- [Projects](projects.md) including setting Webhooks
- [Project Access Requests](access_requests.md)
- [Project Members](members.md)
diff --git a/doc/api/builds.md b/doc/api/builds.md
index 8864df03c98..dce666445d0 100644
--- a/doc/api/builds.md
+++ b/doc/api/builds.md
@@ -532,3 +532,49 @@ Example response:
"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
+}
+```
diff --git a/doc/api/deployments.md b/doc/api/deployments.md
new file mode 100644
index 00000000000..417962de82d
--- /dev/null
+++ b/doc/api/deployments.md
@@ -0,0 +1,218 @@
+# 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
+ }
+}
+```
diff --git a/doc/api/pipelines.md b/doc/api/pipelines.md
new file mode 100644
index 00000000000..847408a7f61
--- /dev/null
+++ b/doc/api/pipelines.md
@@ -0,0 +1,207 @@
+# 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
diff --git a/doc/ci/yaml/README.md b/doc/ci/yaml/README.md
index 01d71088543..e7850aa2c9d 100644
--- a/doc/ci/yaml/README.md
+++ b/doc/ci/yaml/README.md
@@ -353,7 +353,7 @@ job_name:
| 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 |
| 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|
| artifacts | no | Define list of build artifacts |
| cache | no | Define list of files that should be cached between subsequent runs |
diff --git a/doc/legal/corporate_contributor_license_agreement.md b/doc/legal/corporate_contributor_license_agreement.md
index edd6c59138f..7f08188bd65 100644
--- a/doc/legal/corporate_contributor_license_agreement.md
+++ b/doc/legal/corporate_contributor_license_agreement.md
@@ -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.
-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).
@@ -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]".
-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.
diff --git a/features/steps/project/issues/award_emoji.rb b/features/steps/project/issues/award_emoji.rb
index 1498f899cf5..cbe5738e7e4 100644
--- a/features/steps/project/issues/award_emoji.rb
+++ b/features/steps/project/issues/award_emoji.rb
@@ -48,7 +48,7 @@ class Spinach::Features::AwardEmoji < Spinach::FeatureSteps
page.within '.awards' do
expect(page).to have_selector '.js-emoji-btn'
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
diff --git a/features/steps/project/issues/issues.rb b/features/steps/project/issues/issues.rb
index 056462a7152..e21f76d00d9 100644
--- a/features/steps/project/issues/issues.rb
+++ b/features/steps/project/issues/issues.rb
@@ -299,7 +299,7 @@ class Spinach::Features::ProjectIssues < Spinach::FeatureSteps
end
step 'I fill in issue search with \'Rock and roll\'' do
- filter_issue 'Description for issue'
+ filter_issue 'Rock and roll'
end
step 'I should see \'Bugfix1\' in issues' do
diff --git a/lib/api/api.rb b/lib/api/api.rb
index d43af3f24e9..6b8bfbbdae6 100644
--- a/lib/api/api.rb
+++ b/lib/api/api.rb
@@ -43,6 +43,7 @@ module API
mount ::API::CommitStatuses
mount ::API::Commits
mount ::API::DeployKeys
+ mount ::API::Deployments
mount ::API::Environments
mount ::API::Files
mount ::API::Groups
@@ -56,6 +57,7 @@ module API
mount ::API::Milestones
mount ::API::Namespaces
mount ::API::Notes
+ mount ::API::Pipelines
mount ::API::ProjectHooks
mount ::API::ProjectSnippets
mount ::API::Projects
diff --git a/lib/api/builds.rb b/lib/api/builds.rb
index be5a3484ec8..52bdbcae5a8 100644
--- a/lib/api/builds.rb
+++ b/lib/api/builds.rb
@@ -189,6 +189,27 @@ module API
present build, with: Entities::Build,
user_can_download_artifacts: can?(current_user, :read_build, user_project)
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
helpers do
diff --git a/lib/api/deployments.rb b/lib/api/deployments.rb
new file mode 100644
index 00000000000..f782bcaf7e9
--- /dev/null
+++ b/lib/api/deployments.rb
@@ -0,0 +1,40 @@
+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
diff --git a/lib/api/entities.rb b/lib/api/entities.rb
index 055716ab1e3..67420772335 100644
--- a/lib/api/entities.rb
+++ b/lib/api/entities.rb
@@ -502,8 +502,28 @@ module API
expose :key, :value
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
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
class RepoLicense < Grape::Entity
diff --git a/lib/api/pipelines.rb b/lib/api/pipelines.rb
new file mode 100644
index 00000000000..2aae75c471d
--- /dev/null
+++ b/lib/api/pipelines.rb
@@ -0,0 +1,74 @@
+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
diff --git a/spec/factories/ci/pipelines.rb b/spec/factories/ci/pipelines.rb
index 04d66020c87..ac2a1ba5dff 100644
--- a/spec/factories/ci/pipelines.rb
+++ b/spec/factories/ci/pipelines.rb
@@ -1,21 +1,3 @@
-# == 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
factory :ci_empty_pipeline, class: Ci::Pipeline do
ref 'master'
diff --git a/spec/features/boards/boards_spec.rb b/spec/features/boards/boards_spec.rb
index e4c5a10ce7e..8910c50c294 100644
--- a/spec/features/boards/boards_spec.rb
+++ b/spec/features/boards/boards_spec.rb
@@ -147,6 +147,7 @@ describe 'Issue Boards', feature: true, js: true do
expect(page).to have_selector('.card', count: 20)
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).to have_selector('.card', count: 40)
@@ -165,6 +166,8 @@ describe 'Issue Boards', feature: true, js: true do
page.within(find('.board', match: :first)) do
find('.form-control').set issue1.title
+ wait_for_vue_resource(spinner: false)
+
expect(page).to have_selector('.card', count: 1)
end
end
@@ -176,7 +179,11 @@ describe 'Issue Boards', feature: true, js: true do
expect(page).to have_selector('.card', count: 1)
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)
end
end
@@ -189,6 +196,8 @@ describe 'Issue Boards', feature: true, js: true do
expect(page).to have_selector('.card', count: 5)
end
+ wait_for_vue_resource
+
page.within(find('.board:nth-child(2)')) do
expect(page.find('.board-header')).to have_content('3')
expect(page).to have_selector('.card', count: 3)
@@ -263,6 +272,7 @@ describe 'Issue Boards', feature: true, js: true do
context 'new list' do
it 'shows all labels in new list dropdown' do
click_button 'Create new list'
+ wait_for_ajax
page.within('.dropdown-menu-issues-board-new') do
expect(page).to have_content(planning.title)
@@ -273,6 +283,7 @@ describe 'Issue Boards', feature: true, js: true do
it 'creates new list for label' do
click_button 'Create new list'
+ wait_for_ajax
page.within('.dropdown-menu-issues-board-new') do
click_link testing.title
@@ -285,6 +296,7 @@ describe 'Issue Boards', feature: true, js: true do
it 'creates new list for Backlog label' do
click_button 'Create new list'
+ wait_for_ajax
page.within('.dropdown-menu-issues-board-new') do
click_link backlog.title
@@ -297,6 +309,7 @@ describe 'Issue Boards', feature: true, js: true do
it 'creates new list for Done label' do
click_button 'Create new list'
+ wait_for_ajax
page.within('.dropdown-menu-issues-board-new') do
click_link done.title
@@ -314,6 +327,7 @@ describe 'Issue Boards', feature: true, js: true do
end
click_button 'Create new list'
+ wait_for_ajax
page.within('.dropdown-menu-issues-board-new') do
click_link testing.title
@@ -333,6 +347,7 @@ describe 'Issue Boards', feature: true, js: true do
it 'filters by author' do
page.within '.issues-filters' do
click_button('Author')
+ wait_for_ajax
page.within '.dropdown-menu-author' do
click_link(user2.name)
@@ -358,6 +373,7 @@ describe 'Issue Boards', feature: true, js: true do
it 'filters by assignee' do
page.within '.issues-filters' do
click_button('Assignee')
+ wait_for_ajax
page.within '.dropdown-menu-assignee' do
click_link(user.name)
@@ -383,6 +399,7 @@ describe 'Issue Boards', feature: true, js: true do
it 'filters by milestone' do
page.within '.issues-filters' do
click_button('Milestone')
+ wait_for_ajax
page.within '.milestone-filter' do
click_link(milestone.title)
@@ -408,6 +425,7 @@ describe 'Issue Boards', feature: true, js: true do
it 'filters by label' do
page.within '.issues-filters' do
click_button('Label')
+ wait_for_ajax
page.within '.dropdown-menu-labels' do
click_link(testing.title)
@@ -436,6 +454,7 @@ describe 'Issue Boards', feature: true, js: true do
page.within '.issues-filters' do
click_button('Label')
+ wait_for_ajax
page.within '.dropdown-menu-labels' do
click_link(testing.title)
@@ -460,8 +479,9 @@ describe 'Issue Boards', feature: true, js: true do
it 'filters by multiple labels' do
page.within '.issues-filters' do
click_button('Label')
+ wait_for_ajax
- page.within '.dropdown-menu-labels' do
+ page.within(find('.dropdown-menu-labels')) do
click_link(testing.title)
wait_for_vue_resource(spinner: false)
click_link(bug.title)
@@ -486,6 +506,7 @@ describe 'Issue Boards', feature: true, js: true do
it 'filters by no label' do
page.within '.issues-filters' do
click_button('Label')
+ wait_for_ajax
page.within '.dropdown-menu-labels' do
click_link("No Label")
@@ -510,10 +531,13 @@ describe 'Issue Boards', feature: true, js: true do
it 'filters by clicking label button on issue' do
page.within(find('.board', match: :first)) do
expect(page).to have_selector('.card', count: 6)
+ expect(find('.card', match: :first)).to have_content(bug.title)
click_button(bug.title)
wait_for_vue_resource(spinner: false)
end
+ wait_for_vue_resource
+
page.within(find('.board', match: :first)) do
expect(page.find('.board-header')).to have_content('1')
expect(page).to have_selector('.card', count: 1)
diff --git a/spec/features/u2f_spec.rb b/spec/features/u2f_spec.rb
index d370f90f7d9..a46e48c76ed 100644
--- a/spec/features/u2f_spec.rb
+++ b/spec/features/u2f_spec.rb
@@ -12,10 +12,12 @@ feature 'Using U2F (Universal 2nd Factor) Devices for Authentication', feature:
end
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
click_on 'Setup New U2F Device'
expect(page).to have_content('Your device was successfully set up')
+ fill_in "Pick a name", with: name
click_on 'Register U2F Device'
u2f_device
end
@@ -40,13 +42,14 @@ feature 'Using U2F (Universal 2nd Factor) Devices for Authentication', feature:
end
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
manage_two_factor_authentication
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')
end
@@ -55,15 +58,31 @@ feature 'Using U2F (Universal 2nd Factor) Devices for Authentication', feature:
# First device
manage_two_factor_authentication
- register_u2f_device
+ first_device = register_u2f_device
expect(page.body).to match('Your U2F device was registered')
# Second device
- manage_two_factor_authentication
- register_u2f_device
+ second_device = register_u2f_device
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
- 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
@@ -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
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)
unregistered_device.respond_to_u2f_authentication
click_on "Login Via U2F Device"
@@ -262,6 +281,7 @@ feature 'Using U2F (Universal 2nd Factor) Devices for Authentication', feature:
end
it "deletes u2f registrations" do
+ visit profile_account_path
expect { click_on "Disable" }.to change { U2fRegistration.count }.by(-1)
end
end
diff --git a/spec/helpers/blob_helper_spec.rb b/spec/helpers/blob_helper_spec.rb
index 94972eed945..a43a7238c70 100644
--- a/spec/helpers/blob_helper_spec.rb
+++ b/spec/helpers/blob_helper_spec.rb
@@ -69,18 +69,40 @@ describe BlobHelper do
end
describe "#edit_blob_link" do
- let(:project) { create(:project) }
+ let(:namespace) { create(:namespace, name: 'gitlab' )}
+ let(:project) { create(:project, namespace: namespace) }
before do
allow(self).to receive(:current_user).and_return(double)
+ allow(self).to receive(:can_collaborate_with_project?).and_return(true)
end
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')
expect(button).to start_with('<button')
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
diff --git a/spec/helpers/issues_helper_spec.rb b/spec/helpers/issues_helper_spec.rb
index 5e4655dfc95..67bac782591 100644
--- a/spec/helpers/issues_helper_spec.rb
+++ b/spec/helpers/issues_helper_spec.rb
@@ -62,6 +62,32 @@ describe IssuesHelper do
it { is_expected.to eq("!1, !2, or !3") }
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
let!(:upvote) { create(:award_emoji) }
diff --git a/spec/javascripts/abuse_reports_spec.js.es6 b/spec/javascripts/abuse_reports_spec.js.es6
new file mode 100644
index 00000000000..6bcfdf191c2
--- /dev/null
+++ b/spec/javascripts/abuse_reports_spec.js.es6
@@ -0,0 +1,41 @@
+/*= 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);
diff --git a/spec/javascripts/awards_handler_spec.js b/spec/javascripts/awards_handler_spec.js
index 3ddc163033e..fa32d0d7da5 100644
--- a/spec/javascripts/awards_handler_spec.js
+++ b/spec/javascripts/awards_handler_spec.js
@@ -143,6 +143,52 @@
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() {
return it('should filter the emoji', function() {
$('.js-add-award').eq(0).click();
diff --git a/spec/javascripts/fixtures/abuse_reports.html.haml b/spec/javascripts/fixtures/abuse_reports.html.haml
new file mode 100644
index 00000000000..2ec302abcb7
--- /dev/null
+++ b/spec/javascripts/fixtures/abuse_reports.html.haml
@@ -0,0 +1,16 @@
+.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.
diff --git a/spec/requests/api/builds_spec.rb b/spec/requests/api/builds_spec.rb
index 41503885dd9..9a17a705b1e 100644
--- a/spec/requests/api/builds_spec.rb
+++ b/spec/requests/api/builds_spec.rb
@@ -407,4 +407,27 @@ describe API::API, api: true do
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
diff --git a/spec/requests/api/deployments_spec.rb b/spec/requests/api/deployments_spec.rb
new file mode 100644
index 00000000000..8fa8c66db6c
--- /dev/null
+++ b/spec/requests/api/deployments_spec.rb
@@ -0,0 +1,60 @@
+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
diff --git a/spec/requests/api/environments_spec.rb b/spec/requests/api/environments_spec.rb
index 05e57905343..1898b07835d 100644
--- a/spec/requests/api/environments_spec.rb
+++ b/spec/requests/api/environments_spec.rb
@@ -26,6 +26,7 @@ describe API::API, api: true do
expect(json_response.size).to eq(1)
expect(json_response.first['name']).to eq(environment.name)
expect(json_response.first['external_url']).to eq(environment.external_url)
+ expect(json_response.first['project']['id']).to eq(project.id)
end
end
diff --git a/spec/requests/api/pipelines_spec.rb b/spec/requests/api/pipelines_spec.rb
new file mode 100644
index 00000000000..7011bdc9ec0
--- /dev/null
+++ b/spec/requests/api/pipelines_spec.rb
@@ -0,0 +1,133 @@
+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
diff --git a/spec/services/todo_service_spec.rb b/spec/services/todo_service_spec.rb
index 6c3cbeae13c..a2a1d5e6d30 100644
--- a/spec/services/todo_service_spec.rb
+++ b/spec/services/todo_service_spec.rb
@@ -194,12 +194,12 @@ describe TodoService, services: true do
end
end
- describe '#mark_todos_as_done' do
- it 'marks related todos for the user as done' do
- first_todo = create(:todo, :assigned, user: john_doe, project: project, target: issue, author: author)
- second_todo = create(:todo, :assigned, user: john_doe, project: project, target: issue, author: author)
+ shared_examples 'marking todos as done' do |meth|
+ let!(:first_todo) { create(:todo, :assigned, user: john_doe, project: project, target: issue, author: author) }
+ let!(:second_todo) { create(:todo, :assigned, user: john_doe, project: project, target: issue, author: author) }
- service.mark_todos_as_done([first_todo, second_todo], john_doe)
+ it 'marks related todos for the user as done' do
+ service.send(meth, collection, john_doe)
expect(first_todo.reload).to be_done
expect(second_todo.reload).to be_done
@@ -207,20 +207,30 @@ describe TodoService, services: true do
describe 'cached counts' do
it 'updates when todos change' do
- todo = create(:todo, :assigned, user: john_doe, project: project, target: issue, author: author)
-
expect(john_doe.todos_done_count).to eq(0)
- expect(john_doe.todos_pending_count).to eq(1)
+ expect(john_doe.todos_pending_count).to eq(2)
expect(john_doe).to receive(:update_todos_count_cache).and_call_original
- service.mark_todos_as_done([todo], john_doe)
+ service.send(meth, collection, john_doe)
- expect(john_doe.todos_done_count).to eq(1)
+ expect(john_doe.todos_done_count).to eq(2)
expect(john_doe.todos_pending_count).to eq(0)
end
end
end
+ describe '#mark_todos_as_done' do
+ it_behaves_like 'marking todos as done', :mark_todos_as_done do
+ let(:collection) { [first_todo, second_todo] }
+ end
+ end
+
+ describe '#mark_todos_as_done_by_ids' do
+ it_behaves_like 'marking todos as done', :mark_todos_as_done_by_ids do
+ let(:collection) { [first_todo, second_todo].map(&:id) }
+ end
+ end
+
describe '#new_note' do
let!(:first_todo) { create(:todo, :assigned, user: john_doe, project: project, target: issue, author: author) }
let!(:second_todo) { create(:todo, :assigned, user: john_doe, project: project, target: issue, author: author) }
diff --git a/spec/support/fake_u2f_device.rb b/spec/support/fake_u2f_device.rb
index f550e9a0160..8c407b867fe 100644
--- a/spec/support/fake_u2f_device.rb
+++ b/spec/support/fake_u2f_device.rb
@@ -1,6 +1,9 @@
class FakeU2fDevice
- def initialize(page)
+ attr_reader :name
+
+ def initialize(page, name)
@page = page
+ @name = name
end
def respond_to_u2f_registration