Commit a04378a5 authored by Fatih Acet's avatar Fatih Acet

Merge branch 'master' of gitlab.com:gitlab-org/gitlab-ce into revert-c676283b-existing

parents 94d88748 b8005b61
...@@ -8,9 +8,11 @@ v 8.13.0 (unreleased) ...@@ -8,9 +8,11 @@ v 8.13.0 (unreleased)
- Replaced the check sign to arrow in the show build view. !6501 - Replaced the check sign to arrow in the show build view. !6501
- Add a /wip slash command to toggle the Work In Progress status of a merge request. !6259 (tbalthazar) - Add a /wip slash command to toggle the Work In Progress status of a merge request. !6259 (tbalthazar)
- Speed-up group milestones show page - Speed-up group milestones show page
- Keep refs for each deployment
- Log LDAP lookup errors and don't swallow unrelated exceptions. !6103 (Markus Koller) - Log LDAP lookup errors and don't swallow unrelated exceptions. !6103 (Markus Koller)
- Add more tests for calendar contribution (ClemMakesApps) - Add more tests for calendar contribution (ClemMakesApps)
- Avoid database queries on Banzai::ReferenceParser::BaseParser for nodes without references - Avoid database queries on Banzai::ReferenceParser::BaseParser for nodes without references
- Simplify Mentionable concern instance methods
- Fix permission for setting an issue's due date - Fix permission for setting an issue's due date
- Expose expires_at field when sharing project on API - Expose expires_at field when sharing project on API
- Fix issue with page scrolling to top when closing or pinning sidebar (lukehowell) - Fix issue with page scrolling to top when closing or pinning sidebar (lukehowell)
...@@ -27,6 +29,7 @@ v 8.13.0 (unreleased) ...@@ -27,6 +29,7 @@ v 8.13.0 (unreleased)
- Only update issuable labels if they have been changed - Only update issuable labels if they have been changed
- Take filters in account in issuable counters. !6496 - Take filters in account in issuable counters. !6496
- Use custom Ruby images to test builds (registry.dev.gitlab.org/gitlab/gitlab-build-images:*) - Use custom Ruby images to test builds (registry.dev.gitlab.org/gitlab/gitlab-build-images:*)
- Append issue template to existing description !6149 (Joseph Frazier)
- Revoke button in Applications Settings underlines on hover. - Revoke button in Applications Settings underlines on hover.
- Add missing values to linter !6276 (Katarzyna Kobierska Ula Budziszewska) - Add missing values to linter !6276 (Katarzyna Kobierska Ula Budziszewska)
- Fix Long commit messages overflow viewport in file tree - Fix Long commit messages overflow viewport in file tree
...@@ -42,9 +45,13 @@ v 8.13.0 (unreleased) ...@@ -42,9 +45,13 @@ v 8.13.0 (unreleased)
- Notify the Merger about merge after successful build (Dimitris Karakasilis) - Notify the Merger about merge after successful build (Dimitris Karakasilis)
- Fix broken repository 500 errors in project list - Fix broken repository 500 errors in project list
- Close todos when accepting merge requests via the API !6486 (tonygambone) - Close todos when accepting merge requests via the API !6486 (tonygambone)
- Changed Slack service user referencing from full name to username (Sebastian Poxhofer) - Changed Slack service user referencing from full name to username (Sebastian Poxhofer)
- Add Container Registry on/off status to Admin Area !6638 (the-undefined)
v 8.12.4 (unreleased) v 8.12.4 (unreleased)
- Fix type mismatch bug when closing Jira issue
- Fix issues importing services via Import/Export
- Restrict failed login attempts for users with 2FA enabled
- Fix "Copy to clipboard" tooltip to say "Copied!" when clipboard button is clicked. (lukehowell) - Fix "Copy to clipboard" tooltip to say "Copied!" when clipboard button is clicked. (lukehowell)
v 8.12.3 v 8.12.3
...@@ -104,6 +111,7 @@ v 8.12.0 ...@@ -104,6 +111,7 @@ v 8.12.0
- Fix long comments in diffs messing with table width - Fix long comments in diffs messing with table width
- Add spec covering 'Gitlab::Git::committer_hash' !6433 (dandunckelman) - Add spec covering 'Gitlab::Git::committer_hash' !6433 (dandunckelman)
- Fix pagination on user snippets page - Fix pagination on user snippets page
- Honor "fixed layout" preference in more places !6422
- Run CI builds with the permissions of users !5735 - Run CI builds with the permissions of users !5735
- Fix sorting of issues in API - Fix sorting of issues in API
- Fix download artifacts button links !6407 - Fix download artifacts button links !6407
...@@ -144,6 +152,7 @@ v 8.12.0 ...@@ -144,6 +152,7 @@ v 8.12.0
- Increase ci_builds artifacts_size column to 8-byte integer to allow larger files - Increase ci_builds artifacts_size column to 8-byte integer to allow larger files
- Add textarea autoresize after comment (ClemMakesApps) - Add textarea autoresize after comment (ClemMakesApps)
- Do not write SSH public key 'comments' to authorized_keys !6381 - Do not write SSH public key 'comments' to authorized_keys !6381
- Add due date to issue todos
- Refresh todos count cache when an Issue/MR is deleted - Refresh todos count cache when an Issue/MR is deleted
- Fix branches page dropdown sort alignment (ClemMakesApps) - Fix branches page dropdown sort alignment (ClemMakesApps)
- Hides merge request button on branches page is user doesn't have permissions - Hides merge request button on branches page is user doesn't have permissions
......
...@@ -130,7 +130,7 @@ gem 'state_machines-activerecord', '~> 0.4.0' ...@@ -130,7 +130,7 @@ gem 'state_machines-activerecord', '~> 0.4.0'
gem 'after_commit_queue', '~> 1.3.0' gem 'after_commit_queue', '~> 1.3.0'
# Issue tags # Issue tags
gem 'acts-as-taggable-on', '~> 3.4' gem 'acts-as-taggable-on', '~> 4.0'
# Background jobs # Background jobs
gem 'sidekiq', '~> 4.2' gem 'sidekiq', '~> 4.2'
......
...@@ -44,8 +44,8 @@ GEM ...@@ -44,8 +44,8 @@ GEM
minitest (~> 5.1) minitest (~> 5.1)
thread_safe (~> 0.3, >= 0.3.4) thread_safe (~> 0.3, >= 0.3.4)
tzinfo (~> 1.1) tzinfo (~> 1.1)
acts-as-taggable-on (3.5.0) acts-as-taggable-on (4.0.0)
activerecord (>= 3.2, < 5) activerecord (>= 4.0)
addressable (2.3.8) addressable (2.3.8)
after_commit_queue (1.3.0) after_commit_queue (1.3.0)
activerecord (>= 3.0) activerecord (>= 3.0)
...@@ -802,7 +802,7 @@ DEPENDENCIES ...@@ -802,7 +802,7 @@ DEPENDENCIES
RedCloth (~> 4.3.2) RedCloth (~> 4.3.2)
ace-rails-ap (~> 4.1.0) ace-rails-ap (~> 4.1.0)
activerecord-session_store (~> 1.0.0) activerecord-session_store (~> 1.0.0)
acts-as-taggable-on (~> 3.4) acts-as-taggable-on (~> 4.0)
addressable (~> 2.3.8) addressable (~> 2.3.8)
after_commit_queue (~> 1.3.0) after_commit_queue (~> 1.3.0)
akismet (~> 2.0) akismet (~> 2.0)
...@@ -986,4 +986,4 @@ DEPENDENCIES ...@@ -986,4 +986,4 @@ DEPENDENCIES
wikicloth (= 0.8.1) wikicloth (= 0.8.1)
BUNDLED WITH BUNDLED WITH
1.13.1 1.13.2
...@@ -72,9 +72,17 @@ ...@@ -72,9 +72,17 @@
// To be implemented on the extending class // To be implemented on the extending class
// e.g. // e.g.
// Api.gitignoreText item.name, @requestFileSuccess.bind(@) // Api.gitignoreText item.name, @requestFileSuccess.bind(@)
TemplateSelector.prototype.requestFileSuccess = function(file, skipFocus) { TemplateSelector.prototype.requestFileSuccess = function(file, opts) {
this.editor.setValue(file.content, 1); var oldValue = this.editor.getValue();
if (!skipFocus) this.editor.focus(); var newValue = file.content;
if (opts == null) {
opts = {};
}
if (opts.append && oldValue.length && oldValue !== newValue) {
newValue = oldValue + '\n\n' + newValue;
}
this.editor.setValue(newValue, 1);
if (!opts.skipFocus) this.editor.focus();
if (this.editor instanceof jQuery) { if (this.editor instanceof jQuery) {
this.editor.get(0).dispatchEvent(this.autosizeUpdateEvent); this.editor.get(0).dispatchEvent(this.autosizeUpdateEvent);
......
...@@ -7,6 +7,9 @@ ...@@ -7,6 +7,9 @@
function Diff() { function Diff() {
$('.files .diff-file').singleFileDiff(); $('.files .diff-file').singleFileDiff();
this.filesCommentButton = $('.files .diff-file').filesCommentButton(); this.filesCommentButton = $('.files .diff-file').filesCommentButton();
if (this.diffViewType() === 'parallel') {
$('.content-wrapper .container-fluid').removeClass('container-limited');
}
$(document).off('click', '.js-unfold'); $(document).off('click', '.js-unfold');
$(document).on('click', '.js-unfold', (function(_this) { $(document).on('click', '.js-unfold', (function(_this) {
return function(event) { return function(event) {
...@@ -52,6 +55,10 @@ ...@@ -52,6 +55,10 @@
})(this)); })(this));
} }
Diff.prototype.diffViewType = function() {
return $('.inline-parallel-buttons a.active').data('view-type');
}
Diff.prototype.lineNumbers = function(line) { Diff.prototype.lineNumbers = function(line) {
if (!line.children().length) { if (!line.children().length) {
return [0, 0]; return [0, 0];
......
...@@ -7,13 +7,16 @@ const ORIGIN_BUTTON_TITLE = 'Use theirs'; ...@@ -7,13 +7,16 @@ const ORIGIN_BUTTON_TITLE = 'Use theirs';
class MergeConflictDataProvider { class MergeConflictDataProvider {
getInitialData() { getInitialData() {
// TODO: remove reliance on jQuery and DOM state introspection
const diffViewType = $.cookie('diff_view'); const diffViewType = $.cookie('diff_view');
const fixedLayout = $('.content-wrapper .container-fluid').hasClass('container-limited');
return { return {
isLoading : true, isLoading : true,
hasError : false, hasError : false,
isParallel : diffViewType === 'parallel', isParallel : diffViewType === 'parallel',
diffViewType : diffViewType, diffViewType : diffViewType,
fixedLayout : fixedLayout,
isSubmitting : false, isSubmitting : false,
conflictsData : {}, conflictsData : {},
resolutionData : {} resolutionData : {}
...@@ -192,14 +195,17 @@ class MergeConflictDataProvider { ...@@ -192,14 +195,17 @@ class MergeConflictDataProvider {
updateViewType(newType) { updateViewType(newType) {
const vi = this.vueInstance; const vi = this.vueInstance;
if (newType === vi.diffView || !(newType === 'parallel' || newType === 'inline')) { if (newType === vi.diffViewType || !(newType === 'parallel' || newType === 'inline')) {
return; return;
} }
vi.diffView = newType; vi.diffViewType = newType;
vi.isParallel = newType === 'parallel'; vi.isParallel = newType === 'parallel';
$.cookie('diff_view', newType); // TODO: Make sure that cookie path added. $.cookie('diff_view', newType, {
$('.content-wrapper .container-fluid').toggleClass('container-limited'); path: (gon && gon.relative_url_root) || '/'
});
$('.content-wrapper .container-fluid')
.toggleClass('container-limited', !vi.isParallel && vi.fixedLayout);
} }
......
...@@ -60,9 +60,8 @@ class MergeConflictResolver { ...@@ -60,9 +60,8 @@ class MergeConflictResolver {
$('#conflicts .js-syntax-highlight').syntaxHighlight(); $('#conflicts .js-syntax-highlight').syntaxHighlight();
}); });
if (this.vue.diffViewType === 'parallel') { $('.content-wrapper .container-fluid')
$('.content-wrapper .container-fluid').removeClass('container-limited'); .toggleClass('container-limited', !this.vue.isParallel && this.vue.fixedLayout);
}
}) })
} }
......
...@@ -36,13 +36,10 @@ ...@@ -36,13 +36,10 @@
}; };
MergeRequest.prototype.initTabs = function() { MergeRequest.prototype.initTabs = function() {
if (this.opts.action !== 'new') { if (window.mrTabs) {
// `MergeRequests#new` has no tab-persisting or lazy-loading behavior window.mrTabs.unbindEvents();
window.mrTabs = new MergeRequestTabs(this.opts);
} else {
// Show the first tab (Commits)
return $('.merge-request-tabs a[data-toggle="tab"]:first').tab('show');
} }
window.mrTabs = new MergeRequestTabs(this.opts);
}; };
MergeRequest.prototype.showAllCommits = function() { MergeRequest.prototype.showAllCommits = function() {
......
...@@ -56,6 +56,8 @@ ...@@ -56,6 +56,8 @@
MergeRequestTabs.prototype.commitsLoaded = false; MergeRequestTabs.prototype.commitsLoaded = false;
MergeRequestTabs.prototype.fixedLayoutPref = null;
function MergeRequestTabs(opts) { function MergeRequestTabs(opts) {
this.opts = opts != null ? opts : {}; this.opts = opts != null ? opts : {};
this.opts.setUrl = this.opts.setUrl !== undefined ? this.opts.setUrl : true; this.opts.setUrl = this.opts.setUrl !== undefined ? this.opts.setUrl : true;
...@@ -70,7 +72,12 @@ ...@@ -70,7 +72,12 @@
MergeRequestTabs.prototype.bindEvents = function() { MergeRequestTabs.prototype.bindEvents = function() {
$(document).on('shown.bs.tab', '.merge-request-tabs a[data-toggle="tab"]', this.tabShown); $(document).on('shown.bs.tab', '.merge-request-tabs a[data-toggle="tab"]', this.tabShown);
return $(document).on('click', '.js-show-tab', this.showTab); $(document).on('click', '.js-show-tab', this.showTab);
};
MergeRequestTabs.prototype.unbindEvents = function() {
$(document).off('shown.bs.tab', '.merge-request-tabs a[data-toggle="tab"]', this.tabShown);
$(document).off('click', '.js-show-tab', this.showTab);
}; };
MergeRequestTabs.prototype.showTab = function(event) { MergeRequestTabs.prototype.showTab = function(event) {
...@@ -85,11 +92,15 @@ ...@@ -85,11 +92,15 @@
if (action === 'commits') { if (action === 'commits') {
this.loadCommits($target.attr('href')); this.loadCommits($target.attr('href'));
this.expandView(); this.expandView();
this.resetViewContainer();
} else if (action === 'diffs') { } else if (action === 'diffs') {
this.loadDiff($target.attr('href')); this.loadDiff($target.attr('href'));
if ((typeof bp !== "undefined" && bp !== null) && bp.getBreakpointSize() !== 'lg') { if ((typeof bp !== "undefined" && bp !== null) && bp.getBreakpointSize() !== 'lg') {
this.shrinkView(); this.shrinkView();
} }
if (this.diffViewType() === 'parallel') {
this.expandViewContainer();
}
navBarHeight = $('.navbar-gitlab').outerHeight(); navBarHeight = $('.navbar-gitlab').outerHeight();
$.scrollTo(".merge-request-details .merge-request-tabs", { $.scrollTo(".merge-request-details .merge-request-tabs", {
offset: -navBarHeight offset: -navBarHeight
...@@ -97,11 +108,14 @@ ...@@ -97,11 +108,14 @@
} else if (action === 'builds') { } else if (action === 'builds') {
this.loadBuilds($target.attr('href')); this.loadBuilds($target.attr('href'));
this.expandView(); this.expandView();
this.resetViewContainer();
} else if (action === 'pipelines') { } else if (action === 'pipelines') {
this.loadPipelines($target.attr('href')); this.loadPipelines($target.attr('href'));
this.expandView(); this.expandView();
this.resetViewContainer();
} else { } else {
this.expandView(); this.expandView();
this.resetViewContainer();
} }
if (this.opts.setUrl) { if (this.opts.setUrl) {
this.setCurrentAction(action); this.setCurrentAction(action);
...@@ -126,7 +140,7 @@ ...@@ -126,7 +140,7 @@
if (action === 'show') { if (action === 'show') {
action = 'notes'; action = 'notes';
} }
return $(".merge-request-tabs a[data-action='" + action + "']").tab('show'); $(".merge-request-tabs a[data-action='" + action + "']").tab('show').trigger('shown.bs.tab');
}; };
// Replaces the current Merge Request-specific action in the URL with a new one // Replaces the current Merge Request-specific action in the URL with a new one
...@@ -209,7 +223,7 @@ ...@@ -209,7 +223,7 @@
gl.utils.localTimeAgo($('.js-timeago', 'div#diffs')); gl.utils.localTimeAgo($('.js-timeago', 'div#diffs'));
$('#diffs .js-syntax-highlight').syntaxHighlight(); $('#diffs .js-syntax-highlight').syntaxHighlight();
$('#diffs .diff-file').singleFileDiff(); $('#diffs .diff-file').singleFileDiff();
if (_this.diffViewType() === 'parallel') { if (_this.diffViewType() === 'parallel' && _this.currentAction === 'diffs') {
_this.expandViewContainer(); _this.expandViewContainer();
} }
_this.diffsLoaded = true; _this.diffsLoaded = true;
...@@ -308,11 +322,21 @@ ...@@ -308,11 +322,21 @@
MergeRequestTabs.prototype.diffViewType = function() { MergeRequestTabs.prototype.diffViewType = function() {
return $('.inline-parallel-buttons a.active').data('view-type'); return $('.inline-parallel-buttons a.active').data('view-type');
// Returns diff view type
}; };
MergeRequestTabs.prototype.expandViewContainer = function() { MergeRequestTabs.prototype.expandViewContainer = function() {
return $('.container-fluid').removeClass('container-limited'); var $wrapper = $('.content-wrapper .container-fluid');
if (this.fixedLayoutPref === null) {
this.fixedLayoutPref = $wrapper.hasClass('container-limited');
}
$wrapper.removeClass('container-limited');
};
MergeRequestTabs.prototype.resetViewContainer = function() {
if (this.fixedLayoutPref !== null) {
$('.content-wrapper .container-fluid')
.toggleClass('container-limited', this.fixedLayoutPref);
}
}; };
MergeRequestTabs.prototype.shrinkView = function() { MergeRequestTabs.prototype.shrinkView = function() {
......
...@@ -16,7 +16,7 @@ ...@@ -16,7 +16,7 @@
if (initialQuery.name) this.requestFile(initialQuery); if (initialQuery.name) this.requestFile(initialQuery);
$('.reset-template', this.dropdown.parent()).on('click', () => { $('.reset-template', this.dropdown.parent()).on('click', () => {
if (this.currentTemplate) this.setInputValueToTemplateContent(); if (this.currentTemplate) this.setInputValueToTemplateContent(false);
}); });
} }
...@@ -26,22 +26,24 @@ ...@@ -26,22 +26,24 @@
this.currentTemplate = currentTemplate; this.currentTemplate = currentTemplate;
if (err) return; // Error handled by global AJAX error handler if (err) return; // Error handled by global AJAX error handler
this.stopLoadingSpinner(); this.stopLoadingSpinner();
this.setInputValueToTemplateContent(); this.setInputValueToTemplateContent(true);
}); });
return; return;
} }
setInputValueToTemplateContent() { setInputValueToTemplateContent(append) {
// `this.requestFileSuccess` sets the value of the description input field // `this.requestFileSuccess` sets the value of the description input field
// to the content of the template selected. // to the content of the template selected. If `append` is true, the
// template content will be appended to the previous value of the field,
// separated by a blank line if the previous value is non-empty.
if (this.titleInput.val() === '') { if (this.titleInput.val() === '') {
// If the title has not yet been set, focus the title input and // If the title has not yet been set, focus the title input and
// skip focusing the description input by setting `true` as the 2nd // skip focusing the description input by setting `true` as the
// argument to `requestFileSuccess`. // `skipFocus` option to `requestFileSuccess`.
this.requestFileSuccess(this.currentTemplate, true); this.requestFileSuccess(this.currentTemplate, {skipFocus: true, append});
this.titleInput.focus(); this.titleInput.focus();
} else { } else {
this.requestFileSuccess(this.currentTemplate); this.requestFileSuccess(this.currentTemplate, {skipFocus: false, append});
} }
return; return;
} }
......
...@@ -23,15 +23,24 @@ module AuthenticatesWithTwoFactor ...@@ -23,15 +23,24 @@ module AuthenticatesWithTwoFactor
# #
# Returns nil # Returns nil
def prompt_for_two_factor(user) def prompt_for_two_factor(user)
return locked_user_redirect(user) if user.access_locked?
session[:otp_user_id] = user.id session[:otp_user_id] = user.id
setup_u2f_authentication(user) setup_u2f_authentication(user)
render 'devise/sessions/two_factor' render 'devise/sessions/two_factor'
end end
def locked_user_redirect(user)
flash.now[:alert] = 'Invalid Login or password'
render 'devise/sessions/new'
end
def authenticate_with_two_factor def authenticate_with_two_factor
user = self.resource = find_user user = self.resource = find_user
if user_params[:otp_attempt].present? && session[:otp_user_id] if user.access_locked?
locked_user_redirect(user)
elsif user_params[:otp_attempt].present? && session[:otp_user_id]
authenticate_with_two_factor_via_otp(user) authenticate_with_two_factor_via_otp(user)
elsif user_params[:device_response].present? && session[:otp_user_id] elsif user_params[:device_response].present? && session[:otp_user_id]
authenticate_with_two_factor_via_u2f(user) authenticate_with_two_factor_via_u2f(user)
...@@ -50,8 +59,9 @@ module AuthenticatesWithTwoFactor ...@@ -50,8 +59,9 @@ module AuthenticatesWithTwoFactor
remember_me(user) if user_params[:remember_me] == '1' remember_me(user) if user_params[:remember_me] == '1'
sign_in(user) sign_in(user)
else else
user.increment_failed_attempts!
flash.now[:alert] = 'Invalid two-factor code.' flash.now[:alert] = 'Invalid two-factor code.'
render :two_factor prompt_for_two_factor(user)
end end
end end
...@@ -65,6 +75,7 @@ module AuthenticatesWithTwoFactor ...@@ -65,6 +75,7 @@ module AuthenticatesWithTwoFactor
remember_me(user) if user_params[:remember_me] == '1' remember_me(user) if user_params[:remember_me] == '1'
sign_in(user) sign_in(user)
else else
user.increment_failed_attempts!
flash.now[:alert] = 'Authentication via U2F device failed.' flash.now[:alert] = 'Authentication via U2F device failed.'
prompt_for_two_factor(user) prompt_for_two_factor(user)
end end
......
...@@ -15,18 +15,17 @@ module MembershipActions ...@@ -15,18 +15,17 @@ module MembershipActions
end end
def leave def leave
@member = membershipable.members.find_by(user_id: current_user) || member = Members::DestroyService.new(membershipable, current_user, user_id: current_user.id).
membershipable.requesters.find_by(user_id: current_user) execute(:all)
Members::DestroyService.new(@member, current_user).execute
source_type = @member.real_source_type.humanize(capitalize: false) source_type = membershipable.class.to_s.humanize(capitalize: false)
notice = notice =
if @member.request? if member.request?
"Your access request to the #{source_type} has been withdrawn." "Your access request to the #{source_type} has been withdrawn."
else else
"You left the \"#{@member.source.human_name}\" #{source_type}." "You left the \"#{membershipable.human_name}\" #{source_type}."
end end
redirect_path = @member.request? ? @member.source : [:dashboard, @member.real_source_type.tableize] redirect_path = member.request? ? member.source : [:dashboard, membershipable.class.to_s.tableize]
redirect_to redirect_path, notice: notice redirect_to redirect_path, notice: notice
end end
......
...@@ -40,10 +40,7 @@ class Groups::GroupMembersController < Groups::ApplicationController ...@@ -40,10 +40,7 @@ class Groups::GroupMembersController < Groups::ApplicationController
end end
def destroy def destroy
@group_member = @group.members.find_by(id: params[:id]) || Members::DestroyService.new(@group, current_user, id: params[:id]).execute(:all)
@group.requesters.find_by(id: params[:id])
Members::DestroyService.new(@group_member, current_user).execute
respond_to do |format| respond_to do |format|
format.html { redirect_to group_group_members_path(@group), notice: 'User was successfully removed from group.' } format.html { redirect_to group_group_members_path(@group), notice: 'User was successfully removed from group.' }
......
...@@ -55,10 +55,8 @@ class Projects::ProjectMembersController < Projects::ApplicationController ...@@ -55,10 +55,8 @@ class Projects::ProjectMembersController < Projects::ApplicationController
end end
def destroy def destroy
@project_member = @project.members.find_by(id: params[:id]) || Members::DestroyService.new(@project, current_user, params).
@project.requesters.find_by(id: params[:id]) execute(:all)
Members::DestroyService.new(@project_member, current_user).execute
respond_to do |format| respond_to do |format|
format.html do format.html do
......
...@@ -92,12 +92,8 @@ module PageLayoutHelper ...@@ -92,12 +92,8 @@ module PageLayoutHelper
end end
end end
def fluid_layout(enabled = false) def fluid_layout
if @fluid_layout.nil? current_user && current_user.layout == "fluid"
@fluid_layout = (current_user && current_user.layout == "fluid") || enabled
else
@fluid_layout
end
end end
def blank_container(enabled = false) def blank_container(enabled = false)
......
...@@ -114,6 +114,26 @@ module TodosHelper ...@@ -114,6 +114,26 @@ module TodosHelper
selected_type ? selected_type[:text] : default_type selected_type ? selected_type[:text] : default_type
end end
def todo_due_date(todo)
return unless todo.target.try(:due_date)
is_due_today = todo.target.due_date.today?
is_overdue = todo.target.overdue?
css_class =
if is_due_today
'text-warning'
elsif is_overdue
'text-danger'
else
''
end
html = "&middot; ".html_safe
html << content_tag(:span, class: css_class) do
"Due #{is_due_today ? "today" : todo.target.due_date.to_s(:medium)}"
end
end
private private
def show_todo_state?(todo) def show_todo_state?(todo)
......
...@@ -43,19 +43,15 @@ module Mentionable ...@@ -43,19 +43,15 @@ module Mentionable
self self
end end
def all_references(current_user = nil, text = nil, extractor: nil) def all_references(current_user = nil, extractor: nil)
extractor ||= Gitlab::ReferenceExtractor. extractor ||= Gitlab::ReferenceExtractor.
new(project, current_user) new(project, current_user)
if text self.class.mentionable_attrs.each do |attr, options|
extractor.analyze(text, author: author) text = __send__(attr)
else options = options.merge(cache_key: [self, attr], author: author)
self.class.mentionable_attrs.each do |attr, options|
text = __send__(attr)
options = options.merge(cache_key: [self, attr], author: author)
extractor.analyze(text, options) extractor.analyze(text, options)
end
end end
extractor extractor
...@@ -66,8 +62,8 @@ module Mentionable ...@@ -66,8 +62,8 @@ module Mentionable
end end
# Extract GFM references to other Mentionables from this Mentionable. Always excludes its #local_reference. # Extract GFM references to other Mentionables from this Mentionable. Always excludes its #local_reference.
def referenced_mentionables(current_user = self.author, text = nil) def referenced_mentionables(current_user = self.author)
refs = all_references(current_user, text) refs = all_references(current_user)
refs = (refs.issues + refs.merge_requests + refs.commits) refs = (refs.issues + refs.merge_requests + refs.commits)
# We're using this method instead of Array diffing because that requires # We're using this method instead of Array diffing because that requires
...@@ -77,8 +73,8 @@ module Mentionable ...@@ -77,8 +73,8 @@ module Mentionable
end end
# Create a cross-reference Note for each GFM reference to another Mentionable found in the +mentionable_attrs+. # Create a cross-reference Note for each GFM reference to another Mentionable found in the +mentionable_attrs+.
def create_cross_references!(author = self.author, without = [], text = nil) def create_cross_references!(author = self.author, without = [])
refs = referenced_mentionables(author, text) refs = referenced_mentionables(author)
# We're using this method instead of Array diffing because that requires # We're using this method instead of Array diffing because that requires
# both of the object's `hash` values to be the same, which may not be the # both of the object's `hash` values to be the same, which may not be the
...@@ -97,10 +93,7 @@ module Mentionable ...@@ -97,10 +93,7 @@ module Mentionable
return if changes.empty? return if changes.empty?
original_text = changes.collect { |_, vals| vals.first }.join(' ') create_cross_references!(author)
preexisting = referenced_mentionables(author, original_text)
create_cross_references!(author, preexisting)
end end
private private
......
...@@ -11,7 +11,7 @@ class Deployment < ActiveRecord::Base ...@@ -11,7 +11,7 @@ class Deployment < ActiveRecord::Base
delegate :name, to: :environment, prefix: true delegate :name, to: :environment, prefix: true
after_save :keep_around_commit after_save :create_ref
def commit def commit
project.commit(sha) project.commit(sha)
...@@ -29,8 +29,8 @@ class Deployment < ActiveRecord::Base ...@@ -29,8 +29,8 @@ class Deployment < ActiveRecord::Base
self == environment.last_deployment self == environment.last_deployment
end end
def keep_around_commit def create_ref
project.repository.keep_around(self.sha) project.repository.create_ref(ref, ref_path)
end end
def manual_actions def manual_actions
...@@ -76,4 +76,10 @@ class Deployment < ActiveRecord::Base ...@@ -76,4 +76,10 @@ class Deployment < ActiveRecord::Base
where.not(id: self.id). where.not(id: self.id).
take take
end end
private
def ref_path
File.join(environment.ref_path, 'deployments', id.to_s)
end
end end
...@@ -47,4 +47,8 @@ class Environment < ActiveRecord::Base ...@@ -47,4 +47,8 @@ class Environment < ActiveRecord::Base
def update_merge_request_metrics? def update_merge_request_metrics?
self.name == "production" self.name == "production"
end end
def ref_path
"refs/environments/#{Shellwords.shellescape(name)}"
end
end end
...@@ -523,9 +523,13 @@ class MergeRequest < ActiveRecord::Base ...@@ -523,9 +523,13 @@ class MergeRequest < ActiveRecord::Base
# `MergeRequestsClosingIssues` model. This is a performance optimization. # `MergeRequestsClosingIssues` model. This is a performance optimization.
# Calculating this information for a number of merge requests requires # Calculating this information for a number of merge requests requires
# running `ReferenceExtractor` on each of them separately. # running `ReferenceExtractor` on each of them separately.
# This optimization does not apply to issues from external sources.
def cache_merge_request_closes_issues!(current_user = self.author) def cache_merge_request_closes_issues!(current_user = self.author)
return if project.has_external_issue_tracker?
transaction do transaction do
self.merge_requests_closing_issues.delete_all self.merge_requests_closing_issues.delete_all
closes_issues(current_user).each do |issue| closes_issues(current_user).each do |issue|
self.merge_requests_closing_issues.create!(issue: issue) self.merge_requests_closing_issues.create!(issue: issue)
end end
......
...@@ -997,6 +997,10 @@ class Repository ...@@ -997,6 +997,10 @@ class Repository
Gitlab::Popen.popen(args, path_to_repo) Gitlab::Popen.popen(args, path_to_repo)
end end
def create_ref(ref, ref_path)
fetch_ref(path_to_repo, ref, ref_path)
end
def update_branch_with_hooks(current_user, branch) def update_branch_with_hooks(current_user, branch)
update_autocrlf_option update_autocrlf_option
......
...@@ -136,6 +136,7 @@ class Service < ActiveRecord::Base ...@@ -136,6 +136,7 @@ class Service < ActiveRecord::Base
end end
def #{arg}=(value) def #{arg}=(value)
self.properties ||= {}
updated_properties['#{arg}'] = #{arg} unless #{arg}_changed? updated_properties['#{arg}'] = #{arg} unless #{arg}_changed?
self.properties['#{arg}'] = value self.properties['#{arg}'] = value
end end
......
...@@ -827,6 +827,22 @@ class User < ActiveRecord::Base ...@@ -827,6 +827,22 @@ class User < ActiveRecord::Base
todos_pending_count(force: true) todos_pending_count(force: true)
end end
# This is copied from Devise::Models::Lockable#valid_for_authentication?, as our auth
# flow means we don't call that automatically (and can't conveniently do so).
#
# See:
# <https://github.com/plataformatec/devise/blob/v4.0.0/lib/devise/models/lockable.rb#L92>
#
def increment_failed_attempts!
self.failed_attempts ||= 0
self.failed_attempts += 1
if attempts_exceeded?
lock_access! unless access_locked?
else
save(validate: false)
end
end
private private
def projects_union(min_access_level = nil) def projects_union(min_access_level = nil)
......
...@@ -14,6 +14,8 @@ module Members ...@@ -14,6 +14,8 @@ module Members
if member.request? && member.user != user if member.request? && member.user != user
notification_service.decline_access_request(member) notification_service.decline_access_request(member)
end end
member
end end
end end
end end
module Members module Members
class DestroyService < BaseService class DestroyService < BaseService
attr_accessor :member, :current_user include MembersHelper
def initialize(member, current_user) attr_accessor :source
@member = member
ALLOWED_SCOPES = %i[members requesters all]
def initialize(source, current_user, params = {})
@source = source
@current_user = current_user @current_user = current_user
@params = params
end end
def execute def execute(scope = :members)
unless member && can?(current_user, "destroy_#{member.type.underscore}".to_sym, member) raise "scope :#{scope} is not allowed!" unless ALLOWED_SCOPES.include?(scope)
raise Gitlab::Access::AccessDeniedError
end member = find_member!(scope)
raise Gitlab::Access::AccessDeniedError unless can_destroy_member?(member)
AuthorizedDestroyService.new(member, current_user).execute AuthorizedDestroyService.new(member, current_user).execute
end end
private
def find_member!(scope)
condition = params[:user_id] ? { user_id: params[:user_id] } : { id: params[:id] }
case scope
when :all
source.members.find_by(condition) ||
source.requesters.find_by!(condition)
else
source.public_send(scope).find_by!(condition)
end
end
def can_destroy_member?(member)
member && can?(current_user, action_member_permission(:destroy, member), member)
end
end end
end end
...@@ -347,7 +347,7 @@ module SystemNoteService ...@@ -347,7 +347,7 @@ module SystemNoteService
notes = notes.where(noteable_id: noteable.id) notes = notes.where(noteable_id: noteable.id)
end end
notes_for_mentioner(mentioner, noteable, notes).count > 0 notes_for_mentioner(mentioner, noteable, notes).exists?
end end
# Build an Array of lines detailing each commit added in a merge request # Build an Array of lines detailing each commit added in a merge request
......
...@@ -63,6 +63,11 @@ ...@@ -63,6 +63,11 @@
Reply by email Reply by email
%span.light.pull-right %span.light.pull-right
= boolean_to_icon Gitlab::IncomingEmail.enabled? = boolean_to_icon Gitlab::IncomingEmail.enabled?
%p
Container Registry
%span.light.pull-right
= boolean_to_icon Gitlab.config.registry.enabled
.col-md-4 .col-md-4
%h4 %h4
Components Components
......
...@@ -19,6 +19,7 @@ ...@@ -19,6 +19,7 @@
(removed) (removed)
&middot; #{time_ago_with_tooltip(todo.created_at)} &middot; #{time_ago_with_tooltip(todo.created_at)}
= todo_due_date(todo)
.todo-body .todo-body
.todo-note .todo-note
......
%header.navbar.navbar-fixed-top.navbar-gitlab{ class: nav_header_class } %header.navbar.navbar-fixed-top.navbar-gitlab{ class: nav_header_class }
%div{ class: fluid_layout ? "container-fluid" : "container-fluid" } %div{ class: "container-fluid" }
.header-content .header-content
%button.side-nav-toggle{ type: 'button', "aria-label" => "Toggle global navigation" } %button.side-nav-toggle{ type: 'button', "aria-label" => "Toggle global navigation" }
%span.sr-only Toggle navigation %span.sr-only Toggle navigation
......
...@@ -4,9 +4,9 @@ $('body').addClass('<%= user_application_theme %>') ...@@ -4,9 +4,9 @@ $('body').addClass('<%= user_application_theme %>')
// Toggle container-fluid class // Toggle container-fluid class
if ('<%= current_user.layout %>' === 'fluid') { if ('<%= current_user.layout %>' === 'fluid') {
$('.content-wrapper').find('.container-fluid').removeClass('container-limited') $('.content-wrapper .container-fluid').removeClass('container-limited')
} else { } else {
$('.content-wrapper').find('.container-fluid').addClass('container-limited') $('.content-wrapper .container-fluid').addClass('container-limited')
} }
// Re-enable the "Save" button // Re-enable the "Save" button
......
- show_whitespace_toggle = local_assigns.fetch(:show_whitespace_toggle, true) - show_whitespace_toggle = local_assigns.fetch(:show_whitespace_toggle, true)
- diff_files = diffs.diff_files - diff_files = diffs.diff_files
- if diff_view == :parallel
- fluid_layout true
.content-block.oneline-block.files-changed .content-block.oneline-block.files-changed
.inline-parallel-buttons .inline-parallel-buttons
......
...@@ -4,9 +4,6 @@ ...@@ -4,9 +4,6 @@
- content_for :page_specific_javascripts do - content_for :page_specific_javascripts do
= page_specific_javascript_tag('diff_notes/diff_notes_bundle.js') = page_specific_javascript_tag('diff_notes/diff_notes_bundle.js')
- if diff_view == :parallel
- fluid_layout true
.merge-request{'data-url' => merge_request_path(@merge_request)} .merge-request{'data-url' => merge_request_path(@merge_request)}
= render "projects/merge_requests/show/mr_title" = render "projects/merge_requests/show/mr_title"
......
...@@ -14,6 +14,19 @@ Defining environments in a project's `.gitlab-ci.yml` lets developers track ...@@ -14,6 +14,19 @@ Defining environments in a project's `.gitlab-ci.yml` lets developers track
Deployments are created when [jobs] deploy versions of code to [environments]. Deployments are created when [jobs] deploy versions of code to [environments].
### Checkout deployments locally
Since 8.13, a reference in the git repository is saved for each deployment. So
knowing what the state is of your current environments is only a `git fetch`
away.
In your git config, append the `[remote "<your-remote>"]` block with an extra
fetch line:
```
fetch = +refs/environments/*:refs/remotes/origin/environments/*
```
## Defining environments ## Defining environments
You can create and delete environments manually in the web interface, but we You can create and delete environments manually in the web interface, but we
......
...@@ -75,9 +75,8 @@ module API ...@@ -75,9 +75,8 @@ module API
required_attributes! [:user_id] required_attributes! [:user_id]
source = find_source(source_type, params[:id]) source = find_source(source_type, params[:id])
access_requester = source.requesters.find_by!(user_id: params[:user_id]) ::Members::DestroyService.new(source, current_user, params).
execute(:requesters)
::Members::DestroyService.new(access_requester, current_user).execute
end end
end end
end end
......
...@@ -134,7 +134,7 @@ module API ...@@ -134,7 +134,7 @@ module API
if member.nil? if member.nil?
{ message: "Access revoked", id: params[:user_id].to_i } { message: "Access revoked", id: params[:user_id].to_i }
else else
::Members::DestroyService.new(member, current_user).execute ::Members::DestroyService.new(source, current_user, params).execute
present member.user, with: Entities::Member, member: member present member.user, with: Entities::Member, member: member
end end
......
...@@ -4,20 +4,18 @@ module API ...@@ -4,20 +4,18 @@ module API
before { authenticate! } before { authenticate! }
resource :namespaces do resource :namespaces do
# Get a namespaces list desc 'Get a namespaces list' do
# success Entities::Namespace
# Example Request: end
# GET /namespaces params do
optional :search, type: String, desc: "Search query for namespaces"
end
get do get do
@namespaces = if current_user.admin namespaces = current_user.admin ? Namespace.all : current_user.namespaces
Namespace.all
else namespaces = namespaces.search(params[:search]) if params[:search].present?
current_user.namespaces
end
@namespaces = @namespaces.search(params[:search]) if params[:search].present?
@namespaces = paginate @namespaces
present @namespaces, with: Entities::Namespace present paginate(namespaces), with: Entities::Namespace
end end
end end
end end
......
...@@ -87,10 +87,10 @@ describe Groups::GroupMembersController do ...@@ -87,10 +87,10 @@ describe Groups::GroupMembersController do
context 'when member is not found' do context 'when member is not found' do
before { sign_in(user) } before { sign_in(user) }
it 'returns 403' do it 'returns 404' do
delete :leave, group_id: group delete :leave, group_id: group
expect(response).to have_http_status(403) expect(response).to have_http_status(404)
end end
end end
......
...@@ -135,11 +135,11 @@ describe Projects::ProjectMembersController do ...@@ -135,11 +135,11 @@ describe Projects::ProjectMembersController do
context 'when member is not found' do context 'when member is not found' do
before { sign_in(user) } before { sign_in(user) }
it 'returns 403' do it 'returns 404' do
delete :leave, namespace_id: project.namespace, delete :leave, namespace_id: project.namespace,
project_id: project project_id: project
expect(response).to have_http_status(403) expect(response).to have_http_status(404)
end end
end end
......
...@@ -109,6 +109,44 @@ describe SessionsController do ...@@ -109,6 +109,44 @@ describe SessionsController do
end end
end end
context 'when the user is on their last attempt' do
before do
user.update(failed_attempts: User.maximum_attempts.pred)
end
context 'when OTP is valid' do
it 'authenticates correctly' do
authenticate_2fa(otp_attempt: user.current_otp)
expect(subject.current_user).to eq user
end
end
context 'when OTP is invalid' do
before { authenticate_2fa(otp_attempt: 'invalid') }
it 'does not authenticate' do
expect(subject.current_user).not_to eq user
end
it 'warns about invalid login' do
expect(response).to set_flash.now[:alert]
.to /Invalid Login or password/
end
it 'locks the user' do
expect(user.reload).to be_access_locked
end
it 'keeps the user locked on future login attempts' do
post(:create, user: { login: user.username, password: user.password })
expect(response)
.to set_flash.now[:alert].to /Invalid Login or password/
end
end
end
context 'when another user does not have 2FA enabled' do context 'when another user does not have 2FA enabled' do
let(:another_user) { create(:user) } let(:another_user) { create(:user) }
......
...@@ -26,7 +26,7 @@ feature 'issuable templates', feature: true, js: true do ...@@ -26,7 +26,7 @@ feature 'issuable templates', feature: true, js: true do
scenario 'user selects "bug" template' do scenario 'user selects "bug" template' do
select_template 'bug' select_template 'bug'
wait_for_ajax wait_for_ajax
preview_template preview_template(template_content)
save_changes save_changes
end end
...@@ -42,6 +42,26 @@ feature 'issuable templates', feature: true, js: true do ...@@ -42,6 +42,26 @@ feature 'issuable templates', feature: true, js: true do
end end
end end
context 'user creates an issue using templates, with a prior description' do
let(:prior_description) { 'test issue description' }
let(:template_content) { 'this is a test "bug" template' }
let(:issue) { create(:issue, author: user, assignee: user, project: project) }
background do
project.repository.commit_file(user, '.gitlab/issue_templates/bug.md', template_content, 'added issue template', 'master', false)
visit edit_namespace_project_issue_path project.namespace, project, issue
fill_in :'issue[title]', with: 'test issue title'
fill_in :'issue[description]', with: prior_description
end
scenario 'user selects "bug" template' do
select_template 'bug'
wait_for_ajax
preview_template("#{prior_description}\n\n#{template_content}")
save_changes
end
end
context 'user creates a merge request using templates' do context 'user creates a merge request using templates' do
let(:template_content) { 'this is a test "feature-proposal" template' } let(:template_content) { 'this is a test "feature-proposal" template' }
let(:merge_request) { create(:merge_request, :with_diffs, source_project: project) } let(:merge_request) { create(:merge_request, :with_diffs, source_project: project) }
...@@ -55,7 +75,7 @@ feature 'issuable templates', feature: true, js: true do ...@@ -55,7 +75,7 @@ feature 'issuable templates', feature: true, js: true do
scenario 'user selects "feature-proposal" template' do scenario 'user selects "feature-proposal" template' do
select_template 'feature-proposal' select_template 'feature-proposal'
wait_for_ajax wait_for_ajax
preview_template preview_template(template_content)
save_changes save_changes
end end
end end
...@@ -82,16 +102,16 @@ feature 'issuable templates', feature: true, js: true do ...@@ -82,16 +102,16 @@ feature 'issuable templates', feature: true, js: true do
scenario 'user selects template' do scenario 'user selects template' do
select_template 'feature-proposal' select_template 'feature-proposal'
wait_for_ajax wait_for_ajax
preview_template preview_template(template_content)
save_changes save_changes
end end
end end
end end
end end
def preview_template def preview_template(expected_content)
click_link 'Preview' click_link 'Preview'
expect(page).to have_content template_content expect(page).to have_content expected_content
end end
def save_changes def save_changes
......
...@@ -4,7 +4,7 @@ describe 'Dashboard Todos', feature: true do ...@@ -4,7 +4,7 @@ describe 'Dashboard Todos', feature: true do
let(:user) { create(:user) } let(:user) { create(:user) }
let(:author) { create(:user) } let(:author) { create(:user) }
let(:project) { create(:project, visibility_level: Gitlab::VisibilityLevel::PUBLIC) } let(:project) { create(:project, visibility_level: Gitlab::VisibilityLevel::PUBLIC) }
let(:issue) { create(:issue) } let(:issue) { create(:issue, due_date: Date.today) }
describe 'GET /dashboard/todos' do describe 'GET /dashboard/todos' do
context 'User does not have todos' do context 'User does not have todos' do
...@@ -28,6 +28,12 @@ describe 'Dashboard Todos', feature: true do ...@@ -28,6 +28,12 @@ describe 'Dashboard Todos', feature: true do
expect(page).to have_selector('.todos-list .todo', count: 1) expect(page).to have_selector('.todos-list .todo', count: 1)
end end
it 'shows due date as today' do
page.within first('.todo') do
expect(page).to have_content 'Due today'
end
end
describe 'deleting the todo' do describe 'deleting the todo' do
before do before do
first('.done-todo').click first('.done-todo').click
......
require 'spec_helper' require 'spec_helper'
describe Mentionable do describe Mentionable do
include Mentionable class Example
include Mentionable
def author attr_accessor :project, :message
nil attr_mentionable :message
def author
nil
end
end end
describe 'references' do describe 'references' do
let(:project) { create(:project) } let(:project) { create(:project) }
let(:mentionable) { Example.new }
it 'excludes JIRA references' do it 'excludes JIRA references' do
allow(project).to receive_messages(jira_tracker?: true) allow(project).to receive_messages(jira_tracker?: true)
expect(referenced_mentionables(project, 'JIRA-123')).to be_empty
mentionable.project = project
mentionable.message = 'JIRA-123'
expect(mentionable.referenced_mentionables).to be_empty
end end
end end
end end
...@@ -39,9 +48,8 @@ describe Issue, "Mentionable" do ...@@ -39,9 +48,8 @@ describe Issue, "Mentionable" do
let(:user) { create(:user) } let(:user) { create(:user) }
def referenced_issues(current_user) def referenced_issues(current_user)
text = "#{private_issue.to_reference(project)} and #{public_issue.to_reference}" issue.title = "#{private_issue.to_reference(project)} and #{public_issue.to_reference}"
issue.referenced_mentionables(current_user)
issue.referenced_mentionables(current_user, text)
end end
context 'when the current user can see the issue' do context 'when the current user can see the issue' do
......
...@@ -86,6 +86,30 @@ describe MergeRequest, models: true do ...@@ -86,6 +86,30 @@ describe MergeRequest, models: true do
end end
end end
describe '#cache_merge_request_closes_issues!' do
before do
subject.project.team << [subject.author, :developer]
subject.target_branch = subject.project.default_branch
end
it 'caches closed issues' do
issue = create :issue, project: subject.project
commit = double('commit1', safe_message: "Fixes #{issue.to_reference}")
allow(subject).to receive(:commits).and_return([commit])
expect { subject.cache_merge_request_closes_issues! }.to change(subject.merge_requests_closing_issues, :count).by(1)
end
it 'does not cache issues from external trackers' do
subject.project.update_attribute(:has_external_issue_tracker, true)
issue = ExternalIssue.new('JIRA-123', subject.project)
commit = double('commit1', safe_message: "Fixes #{issue.to_reference}")
allow(subject).to receive(:commits).and_return([commit])
expect { subject.cache_merge_request_closes_issues! }.not_to change(subject.merge_requests_closing_issues, :count)
end
end
describe '#source_branch_sha' do describe '#source_branch_sha' do
let(:last_branch_commit) { subject.source_project.repository.commit(subject.source_branch) } let(:last_branch_commit) { subject.source_project.repository.commit(subject.source_branch) }
...@@ -522,7 +546,7 @@ describe MergeRequest, models: true do ...@@ -522,7 +546,7 @@ describe MergeRequest, models: true do
end end
it_behaves_like 'an editable mentionable' do it_behaves_like 'an editable mentionable' do
subject { create(:merge_request) } subject { create(:merge_request, :simple) }
let(:backref_text) { "merge request #{subject.to_reference}" } let(:backref_text) { "merge request #{subject.to_reference}" }
let(:set_mentionable_text) { ->(txt){ subject.description = txt } } let(:set_mentionable_text) { ->(txt){ subject.description = txt } }
......
...@@ -320,6 +320,16 @@ describe Repository, models: true do ...@@ -320,6 +320,16 @@ describe Repository, models: true do
end end
end end
describe '#create_ref' do
it 'redirects the call to fetch_ref' do
ref, ref_path = '1', '2'
expect(repository).to receive(:fetch_ref).with(repository.path_to_repo, ref, ref_path)
repository.create_ref(ref, ref_path)
end
end
describe "#changelog" do describe "#changelog" do
before do before do
repository.send(:cache).expire(:changelog) repository.send(:cache).expire(:changelog)
......
...@@ -203,6 +203,23 @@ describe Service, models: true do ...@@ -203,6 +203,23 @@ describe Service, models: true do
end end
end end
describe 'initialize service with no properties' do
let(:service) do
GitlabIssueTrackerService.create(
project: create(:project),
title: 'random title'
)
end
it 'does not raise error' do
expect { service }.not_to raise_error
end
it 'creates the properties' do
expect(service.properties).to eq({ "title" => "random title" })
end
end
describe "callbacks" do describe "callbacks" do
let(:project) { create(:project) } let(:project) { create(:project) }
let!(:service) do let!(:service) do
......
...@@ -195,7 +195,7 @@ describe API::AccessRequests, api: true do ...@@ -195,7 +195,7 @@ describe API::AccessRequests, api: true do
end end
context 'when authenticated as the access requester' do context 'when authenticated as the access requester' do
it 'returns 200' do it 'deletes the access requester' do
expect do expect do
delete api("/#{source_type.pluralize}/#{source.id}/access_requests/#{access_requester.id}", access_requester) delete api("/#{source_type.pluralize}/#{source.id}/access_requests/#{access_requester.id}", access_requester)
...@@ -205,7 +205,7 @@ describe API::AccessRequests, api: true do ...@@ -205,7 +205,7 @@ describe API::AccessRequests, api: true do
end end
context 'when authenticated as a master/owner' do context 'when authenticated as a master/owner' do
it 'returns 200' do it 'deletes the access requester' do
expect do expect do
delete api("/#{source_type.pluralize}/#{source.id}/access_requests/#{access_requester.id}", master) delete api("/#{source_type.pluralize}/#{source.id}/access_requests/#{access_requester.id}", master)
...@@ -213,6 +213,16 @@ describe API::AccessRequests, api: true do ...@@ -213,6 +213,16 @@ describe API::AccessRequests, api: true do
end.to change { source.requesters.count }.by(-1) end.to change { source.requesters.count }.by(-1)
end end
context 'user_id matches a member, not an access requester' do
it 'returns 404' do
expect do
delete api("/#{source_type.pluralize}/#{source.id}/access_requests/#{developer.id}", master)
expect(response).to have_http_status(404)
end.not_to change { source.requesters.count }
end
end
context 'user_id does not match an existing access requester' do context 'user_id does not match an existing access requester' do
it 'returns 404' do it 'returns 404' do
expect do expect do
......
...@@ -2,70 +2,111 @@ require 'spec_helper' ...@@ -2,70 +2,111 @@ require 'spec_helper'
describe Members::DestroyService, services: true do describe Members::DestroyService, services: true do
let(:user) { create(:user) } let(:user) { create(:user) }
let(:project) { create(:project) } let(:member_user) { create(:user) }
let!(:member) { create(:project_member, source: project) } let(:project) { create(:project, :public) }
let(:group) { create(:group, :public) }
context 'when member is nil' do shared_examples 'a service raising ActiveRecord::RecordNotFound' do
before do it 'raises ActiveRecord::RecordNotFound' do
project.team << [user, :developer] expect { described_class.new(source, user, params).execute }.to raise_error(ActiveRecord::RecordNotFound)
end end
end
it 'does not destroy the member' do shared_examples 'a service raising Gitlab::Access::AccessDeniedError' do
expect { destroy_member(nil, user) }.to raise_error(Gitlab::Access::AccessDeniedError) it 'raises Gitlab::Access::AccessDeniedError' do
expect { described_class.new(source, user, params).execute }.to raise_error(Gitlab::Access::AccessDeniedError)
end end
end end
context 'when current user cannot destroy the given member' do shared_examples 'a service destroying a member' do
before do it 'destroys the member' do
project.team << [user, :developer] expect { described_class.new(source, user, params).execute }.to change { source.members.count }.by(-1)
end
context 'when the given member is an access requester' do
before do
source.members.find_by(user_id: member_user).destroy
source.request_access(member_user)
end
let(:access_requester) { source.requesters.find_by(user_id: member_user) }
it_behaves_like 'a service raising ActiveRecord::RecordNotFound'
%i[requesters all].each do |scope|
context "and #{scope} scope is passed" do
it 'destroys the access requester' do
expect { described_class.new(source, user, params).execute(scope) }.to change { source.requesters.count }.by(-1)
end
it 'calls Member#after_decline_request' do
expect_any_instance_of(NotificationService).to receive(:decline_access_request).with(access_requester)
described_class.new(source, user, params).execute(scope)
end
context 'when current user is the member' do
it 'does not call Member#after_decline_request' do
expect_any_instance_of(NotificationService).not_to receive(:decline_access_request).with(access_requester)
described_class.new(source, member_user, params).execute(scope)
end
end
end
end
end end
end
context 'when no member are found' do
let(:params) { { user_id: 42 } }
it 'does not destroy the member' do it_behaves_like 'a service raising ActiveRecord::RecordNotFound' do
expect { destroy_member(member, user) }.to raise_error(Gitlab::Access::AccessDeniedError) let(:source) { project }
end
it_behaves_like 'a service raising ActiveRecord::RecordNotFound' do
let(:source) { group }
end end
end end
context 'when current user can destroy the given member' do context 'when a member is found' do
before do before do
project.team << [user, :master] project.team << [member_user, :developer]
group.add_developer(member_user)
end end
let(:params) { { user_id: member_user.id } }
it 'destroys the member' do context 'when current user cannot destroy the given member' do
destroy_member(member, user) it_behaves_like 'a service raising Gitlab::Access::AccessDeniedError' do
let(:source) { project }
end
expect(member).to be_destroyed it_behaves_like 'a service raising Gitlab::Access::AccessDeniedError' do
let(:source) { group }
end
end end
context 'when the given member is a requester' do context 'when current user can destroy the given member' do
before do before do
member.update_column(:requested_at, Time.now) project.team << [user, :master]
group.add_owner(user)
end end
it 'calls Member#after_decline_request' do it_behaves_like 'a service destroying a member' do
expect_any_instance_of(NotificationService).to receive(:decline_access_request).with(member) let(:source) { project }
destroy_member(member, user)
end end
context 'when current user is the member' do it_behaves_like 'a service destroying a member' do
it 'does not call Member#after_decline_request' do let(:source) { group }
expect_any_instance_of(NotificationService).not_to receive(:decline_access_request).with(member)
destroy_member(member, member.user)
end
end end
context 'when current user is the member and ' do context 'when given a :id' do
it 'does not call Member#after_decline_request' do let(:params) { { id: project.members.find_by!(user_id: user.id).id } }
expect_any_instance_of(NotificationService).not_to receive(:decline_access_request).with(member)
destroy_member(member, member.user) it 'destroys the member' do
expect { described_class.new(project, user, params).execute }.
to change { project.members.count }.by(-1)
end end
end end
end end
end end
def destroy_member(member, user)
Members::DestroyService.new(member, user).execute
end
end end
...@@ -38,6 +38,42 @@ describe MergeRequests::MergeService, services: true do ...@@ -38,6 +38,42 @@ describe MergeRequests::MergeService, services: true do
end end
end end
context 'closes related issues' do
let(:service) { described_class.new(project, user, commit_message: 'Awesome message') }
before do
allow(project).to receive(:default_branch).and_return(merge_request.target_branch)
end
it 'closes GitLab issue tracker issues' do
issue = create :issue, project: project
commit = double('commit', safe_message: "Fixes #{issue.to_reference}")
allow(merge_request).to receive(:commits).and_return([commit])
service.execute(merge_request)
expect(issue.reload.closed?).to be_truthy
end
context 'with JIRA integration' do
include JiraServiceHelper
let(:jira_tracker) { project.create_jira_service }
before { jira_service_settings }
it 'closes issues on JIRA issue tracker' do
jira_issue = ExternalIssue.new('JIRA-123', project)
commit = double('commit', safe_message: "Fixes #{jira_issue.to_reference}")
allow(merge_request).to receive(:commits).and_return([commit])
expect_any_instance_of(JiraService).to receive(:close_issue).with(merge_request, jira_issue).once
service.execute(merge_request)
end
end
end
context 'closes related todos' do context 'closes related todos' do
let(:merge_request) { create(:merge_request, assignee: user, author: user) } let(:merge_request) { create(:merge_request, assignee: user, author: user) }
let(:project) { merge_request.project } let(:project) { merge_request.project }
......
...@@ -9,7 +9,7 @@ shared_context 'mentionable context' do ...@@ -9,7 +9,7 @@ shared_context 'mentionable context' do
let(:author) { subject.author } let(:author) { subject.author }
let(:mentioned_issue) { create(:issue, project: project) } let(:mentioned_issue) { create(:issue, project: project) }
let!(:mentioned_mr) { create(:merge_request, :simple, source_project: project) } let!(:mentioned_mr) { create(:merge_request, source_project: project) }
let(:mentioned_commit) { project.commit("HEAD~1") } let(:mentioned_commit) { project.commit("HEAD~1") }
let(:ext_proj) { create(:project, :public) } let(:ext_proj) { create(:project, :public) }
...@@ -100,6 +100,7 @@ shared_examples 'an editable mentionable' do ...@@ -100,6 +100,7 @@ shared_examples 'an editable mentionable' do
it 'creates new cross-reference notes when the mentionable text is edited' do it 'creates new cross-reference notes when the mentionable text is edited' do
subject.save subject.save
subject.create_cross_references!
new_text = <<-MSG.strip_heredoc new_text = <<-MSG.strip_heredoc
These references already existed: These references already existed:
...@@ -131,6 +132,7 @@ shared_examples 'an editable mentionable' do ...@@ -131,6 +132,7 @@ shared_examples 'an editable mentionable' do
end end
# These two issues are new and should receive reference notes # These two issues are new and should receive reference notes
# In the case of MergeRequests remember that cannot mention commits included in the MergeRequest
new_issues.each do |newref| new_issues.each do |newref|
expect(SystemNoteService).to receive(:cross_reference). expect(SystemNoteService).to receive(:cross_reference).
with(newref, subject.local_reference, author) with(newref, subject.local_reference, author)
......
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