Commit 8b05e8c2 authored by Douwe Maan's avatar Douwe Maan

Merge branch 'master' into diff-line-comment-vuejs

# Conflicts:
#	db/schema.rb
parents b5d0346a d97c8309
......@@ -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
......
......@@ -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'
......
......@@ -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)
......
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 @@
/*= require bootstrap/tooltip */
/*= require bootstrap/popover */
/*= require select2 */
/*= require ace/ace */
/*= require ace-rails-ap */
/*= require ace/ext-searchbox */
/*= require underscore */
/*= require dropzone */
......
(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);
......
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();
});
......@@ -196,6 +196,9 @@
case 'edit':
new Labels();
}
case 'abuse_reports':
new gl.AbuseReports();
break;
}
break;
case 'dashboard':
......
......@@ -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);
......@@ -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;
}
......
......@@ -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;
}
}
}
......@@ -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
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
......
......@@ -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
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
......@@ -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' }
......
......@@ -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)
......
......@@ -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
......
......@@ -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
......
- 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"
- 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'
......@@ -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
......@@ -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>";
......
......@@ -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)
......
- 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)
......@@ -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);
......
......@@ -375,6 +375,8 @@ Rails.application.routes.draw do
patch :skip
end
end
resources :u2f_registrations, only: [:destroy]
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
......@@ -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
......
......@@ -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)
......
......@@ -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
}
```
# 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:
| 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 |
......
......@@ -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.
......@@ -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
......
......@@ -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
......
......@@ -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
......
......@@ -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
......
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
......@@ -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
......
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
# == 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'
......
......@@ -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)
......
......@@ -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
......
......@@ -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
......@@ -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) }
......
/*= 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 @@
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();
......
.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.
......@@ -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
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
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
......
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
......@@ -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) }
......
class FakeU2fDevice
def initialize(page)
attr_reader :name
def initialize(page, name)
@page = page
@name = name
end
def respond_to_u2f_registration
......
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