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)
- 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)
- Speed-up group milestones show page
- Keep refs for each deployment
- Log LDAP lookup errors and don't swallow unrelated exceptions. !6103 (Markus Koller)
- Add more tests for calendar contribution (ClemMakesApps)
- 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
- Expose expires_at field when sharing project on API
- Fix issue with page scrolling to top when closing or pinning sidebar (lukehowell)
......@@ -27,6 +29,7 @@ v 8.13.0 (unreleased)
- Only update issuable labels if they have been changed
- Take filters in account in issuable counters. !6496
- 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.
- Add missing values to linter !6276 (Katarzyna Kobierska Ula Budziszewska)
- Fix Long commit messages overflow viewport in file tree
......@@ -42,9 +45,13 @@ v 8.13.0 (unreleased)
- Notify the Merger about merge after successful build (Dimitris Karakasilis)
- Fix broken repository 500 errors in project list
- 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)
- 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)
v 8.12.3
......@@ -104,6 +111,7 @@ v 8.12.0
- Fix long comments in diffs messing with table width
- Add spec covering 'Gitlab::Git::committer_hash' !6433 (dandunckelman)
- Fix pagination on user snippets page
- Honor "fixed layout" preference in more places !6422
- Run CI builds with the permissions of users !5735
- Fix sorting of issues in API
- Fix download artifacts button links !6407
......@@ -144,6 +152,7 @@ v 8.12.0
- Increase ci_builds artifacts_size column to 8-byte integer to allow larger files
- Add textarea autoresize after comment (ClemMakesApps)
- 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
- Fix branches page dropdown sort alignment (ClemMakesApps)
- Hides merge request button on branches page is user doesn't have permissions
......
......@@ -130,7 +130,7 @@ gem 'state_machines-activerecord', '~> 0.4.0'
gem 'after_commit_queue', '~> 1.3.0'
# Issue tags
gem 'acts-as-taggable-on', '~> 3.4'
gem 'acts-as-taggable-on', '~> 4.0'
# Background jobs
gem 'sidekiq', '~> 4.2'
......
......@@ -44,8 +44,8 @@ GEM
minitest (~> 5.1)
thread_safe (~> 0.3, >= 0.3.4)
tzinfo (~> 1.1)
acts-as-taggable-on (3.5.0)
activerecord (>= 3.2, < 5)
acts-as-taggable-on (4.0.0)
activerecord (>= 4.0)
addressable (2.3.8)
after_commit_queue (1.3.0)
activerecord (>= 3.0)
......@@ -802,7 +802,7 @@ DEPENDENCIES
RedCloth (~> 4.3.2)
ace-rails-ap (~> 4.1.0)
activerecord-session_store (~> 1.0.0)
acts-as-taggable-on (~> 3.4)
acts-as-taggable-on (~> 4.0)
addressable (~> 2.3.8)
after_commit_queue (~> 1.3.0)
akismet (~> 2.0)
......@@ -986,4 +986,4 @@ DEPENDENCIES
wikicloth (= 0.8.1)
BUNDLED WITH
1.13.1
1.13.2
......@@ -72,9 +72,17 @@
// To be implemented on the extending class
// e.g.
// Api.gitignoreText item.name, @requestFileSuccess.bind(@)
TemplateSelector.prototype.requestFileSuccess = function(file, skipFocus) {
this.editor.setValue(file.content, 1);
if (!skipFocus) this.editor.focus();
TemplateSelector.prototype.requestFileSuccess = function(file, opts) {
var oldValue = this.editor.getValue();
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) {
this.editor.get(0).dispatchEvent(this.autosizeUpdateEvent);
......
......@@ -7,6 +7,9 @@
function Diff() {
$('.files .diff-file').singleFileDiff();
this.filesCommentButton = $('.files .diff-file').filesCommentButton();
if (this.diffViewType() === 'parallel') {
$('.content-wrapper .container-fluid').removeClass('container-limited');
}
$(document).off('click', '.js-unfold');
$(document).on('click', '.js-unfold', (function(_this) {
return function(event) {
......@@ -52,6 +55,10 @@
})(this));
}
Diff.prototype.diffViewType = function() {
return $('.inline-parallel-buttons a.active').data('view-type');
}
Diff.prototype.lineNumbers = function(line) {
if (!line.children().length) {
return [0, 0];
......
......@@ -7,13 +7,16 @@ const ORIGIN_BUTTON_TITLE = 'Use theirs';
class MergeConflictDataProvider {
getInitialData() {
// TODO: remove reliance on jQuery and DOM state introspection
const diffViewType = $.cookie('diff_view');
const fixedLayout = $('.content-wrapper .container-fluid').hasClass('container-limited');
return {
isLoading : true,
hasError : false,
isParallel : diffViewType === 'parallel',
diffViewType : diffViewType,
fixedLayout : fixedLayout,
isSubmitting : false,
conflictsData : {},
resolutionData : {}
......@@ -192,14 +195,17 @@ class MergeConflictDataProvider {
updateViewType(newType) {
const vi = this.vueInstance;
if (newType === vi.diffView || !(newType === 'parallel' || newType === 'inline')) {
if (newType === vi.diffViewType || !(newType === 'parallel' || newType === 'inline')) {
return;
}
vi.diffView = newType;
vi.isParallel = newType === 'parallel';
$.cookie('diff_view', newType); // TODO: Make sure that cookie path added.
$('.content-wrapper .container-fluid').toggleClass('container-limited');
vi.diffViewType = newType;
vi.isParallel = newType === 'parallel';
$.cookie('diff_view', newType, {
path: (gon && gon.relative_url_root) || '/'
});
$('.content-wrapper .container-fluid')
.toggleClass('container-limited', !vi.isParallel && vi.fixedLayout);
}
......
......@@ -60,9 +60,8 @@ class MergeConflictResolver {
$('#conflicts .js-syntax-highlight').syntaxHighlight();
});
if (this.vue.diffViewType === 'parallel') {
$('.content-wrapper .container-fluid').removeClass('container-limited');
}
$('.content-wrapper .container-fluid')
.toggleClass('container-limited', !this.vue.isParallel && this.vue.fixedLayout);
})
}
......
......@@ -36,13 +36,10 @@
};
MergeRequest.prototype.initTabs = function() {
if (this.opts.action !== 'new') {
// `MergeRequests#new` has no tab-persisting or lazy-loading behavior
window.mrTabs = new MergeRequestTabs(this.opts);
} else {
// Show the first tab (Commits)
return $('.merge-request-tabs a[data-toggle="tab"]:first').tab('show');
if (window.mrTabs) {
window.mrTabs.unbindEvents();
}
window.mrTabs = new MergeRequestTabs(this.opts);
};
MergeRequest.prototype.showAllCommits = function() {
......
......@@ -56,6 +56,8 @@
MergeRequestTabs.prototype.commitsLoaded = false;
MergeRequestTabs.prototype.fixedLayoutPref = null;
function MergeRequestTabs(opts) {
this.opts = opts != null ? opts : {};
this.opts.setUrl = this.opts.setUrl !== undefined ? this.opts.setUrl : true;
......@@ -70,7 +72,12 @@
MergeRequestTabs.prototype.bindEvents = function() {
$(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) {
......@@ -85,11 +92,15 @@
if (action === 'commits') {
this.loadCommits($target.attr('href'));
this.expandView();
this.resetViewContainer();
} else if (action === 'diffs') {
this.loadDiff($target.attr('href'));
if ((typeof bp !== "undefined" && bp !== null) && bp.getBreakpointSize() !== 'lg') {
this.shrinkView();
}
if (this.diffViewType() === 'parallel') {
this.expandViewContainer();
}
navBarHeight = $('.navbar-gitlab').outerHeight();
$.scrollTo(".merge-request-details .merge-request-tabs", {
offset: -navBarHeight
......@@ -97,11 +108,14 @@
} else if (action === 'builds') {
this.loadBuilds($target.attr('href'));
this.expandView();
this.resetViewContainer();
} else if (action === 'pipelines') {
this.loadPipelines($target.attr('href'));
this.expandView();
this.resetViewContainer();
} else {
this.expandView();
this.resetViewContainer();
}
if (this.opts.setUrl) {
this.setCurrentAction(action);
......@@ -126,7 +140,7 @@
if (action === 'show') {
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
......@@ -209,7 +223,7 @@
gl.utils.localTimeAgo($('.js-timeago', 'div#diffs'));
$('#diffs .js-syntax-highlight').syntaxHighlight();
$('#diffs .diff-file').singleFileDiff();
if (_this.diffViewType() === 'parallel') {
if (_this.diffViewType() === 'parallel' && _this.currentAction === 'diffs') {
_this.expandViewContainer();
}
_this.diffsLoaded = true;
......@@ -308,11 +322,21 @@
MergeRequestTabs.prototype.diffViewType = function() {
return $('.inline-parallel-buttons a.active').data('view-type');
// Returns diff view type
};
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() {
......
......@@ -16,7 +16,7 @@
if (initialQuery.name) this.requestFile(initialQuery);
$('.reset-template', this.dropdown.parent()).on('click', () => {
if (this.currentTemplate) this.setInputValueToTemplateContent();
if (this.currentTemplate) this.setInputValueToTemplateContent(false);
});
}
......@@ -26,22 +26,24 @@
this.currentTemplate = currentTemplate;
if (err) return; // Error handled by global AJAX error handler
this.stopLoadingSpinner();
this.setInputValueToTemplateContent();
this.setInputValueToTemplateContent(true);
});
return;
}
setInputValueToTemplateContent() {
setInputValueToTemplateContent(append) {
// `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 the title has not yet been set, focus the title input and
// skip focusing the description input by setting `true` as the 2nd
// argument to `requestFileSuccess`.
this.requestFileSuccess(this.currentTemplate, true);
// skip focusing the description input by setting `true` as the
// `skipFocus` option to `requestFileSuccess`.
this.requestFileSuccess(this.currentTemplate, {skipFocus: true, append});
this.titleInput.focus();
} else {
this.requestFileSuccess(this.currentTemplate);
this.requestFileSuccess(this.currentTemplate, {skipFocus: false, append});
}
return;
}
......
......@@ -23,15 +23,24 @@ module AuthenticatesWithTwoFactor
#
# Returns nil
def prompt_for_two_factor(user)
return locked_user_redirect(user) if user.access_locked?
session[:otp_user_id] = user.id
setup_u2f_authentication(user)
render 'devise/sessions/two_factor'
end
def locked_user_redirect(user)
flash.now[:alert] = 'Invalid Login or password'
render 'devise/sessions/new'
end
def authenticate_with_two_factor
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)
elsif user_params[:device_response].present? && session[:otp_user_id]
authenticate_with_two_factor_via_u2f(user)
......@@ -50,8 +59,9 @@ module AuthenticatesWithTwoFactor
remember_me(user) if user_params[:remember_me] == '1'
sign_in(user)
else
user.increment_failed_attempts!
flash.now[:alert] = 'Invalid two-factor code.'
render :two_factor
prompt_for_two_factor(user)
end
end
......@@ -65,6 +75,7 @@ module AuthenticatesWithTwoFactor
remember_me(user) if user_params[:remember_me] == '1'
sign_in(user)
else
user.increment_failed_attempts!
flash.now[:alert] = 'Authentication via U2F device failed.'
prompt_for_two_factor(user)
end
......
......@@ -15,18 +15,17 @@ module MembershipActions
end
def leave
@member = membershipable.members.find_by(user_id: current_user) ||
membershipable.requesters.find_by(user_id: current_user)
Members::DestroyService.new(@member, current_user).execute
member = Members::DestroyService.new(membershipable, current_user, user_id: current_user.id).
execute(:all)
source_type = @member.real_source_type.humanize(capitalize: false)
source_type = membershipable.class.to_s.humanize(capitalize: false)
notice =
if @member.request?
if member.request?
"Your access request to the #{source_type} has been withdrawn."
else
"You left the \"#{@member.source.human_name}\" #{source_type}."
"You left the \"#{membershipable.human_name}\" #{source_type}."
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
end
......
......@@ -40,10 +40,7 @@ class Groups::GroupMembersController < Groups::ApplicationController
end
def destroy
@group_member = @group.members.find_by(id: params[:id]) ||
@group.requesters.find_by(id: params[:id])
Members::DestroyService.new(@group_member, current_user).execute
Members::DestroyService.new(@group, current_user, id: params[:id]).execute(:all)
respond_to do |format|
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
end
def destroy
@project_member = @project.members.find_by(id: params[:id]) ||
@project.requesters.find_by(id: params[:id])
Members::DestroyService.new(@project_member, current_user).execute
Members::DestroyService.new(@project, current_user, params).
execute(:all)
respond_to do |format|
format.html do
......
......@@ -92,12 +92,8 @@ module PageLayoutHelper
end
end
def fluid_layout(enabled = false)
if @fluid_layout.nil?
@fluid_layout = (current_user && current_user.layout == "fluid") || enabled
else
@fluid_layout
end
def fluid_layout
current_user && current_user.layout == "fluid"
end
def blank_container(enabled = false)
......
......@@ -114,6 +114,26 @@ module TodosHelper
selected_type ? selected_type[:text] : default_type
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
def show_todo_state?(todo)
......
......@@ -43,19 +43,15 @@ module Mentionable
self
end
def all_references(current_user = nil, text = nil, extractor: nil)
def all_references(current_user = nil, extractor: nil)
extractor ||= Gitlab::ReferenceExtractor.
new(project, current_user)
if text
extractor.analyze(text, author: author)
else
self.class.mentionable_attrs.each do |attr, options|
text = __send__(attr)
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)
end
extractor.analyze(text, options)
end
extractor
......@@ -66,8 +62,8 @@ module Mentionable
end
# Extract GFM references to other Mentionables from this Mentionable. Always excludes its #local_reference.
def referenced_mentionables(current_user = self.author, text = nil)
refs = all_references(current_user, text)
def referenced_mentionables(current_user = self.author)
refs = all_references(current_user)
refs = (refs.issues + refs.merge_requests + refs.commits)
# We're using this method instead of Array diffing because that requires
......@@ -77,8 +73,8 @@ module Mentionable
end
# 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)
refs = referenced_mentionables(author, text)
def create_cross_references!(author = self.author, without = [])
refs = referenced_mentionables(author)
# 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
......@@ -97,10 +93,7 @@ module Mentionable
return if changes.empty?
original_text = changes.collect { |_, vals| vals.first }.join(' ')
preexisting = referenced_mentionables(author, original_text)
create_cross_references!(author, preexisting)
create_cross_references!(author)
end
private
......
......@@ -11,7 +11,7 @@ class Deployment < ActiveRecord::Base
delegate :name, to: :environment, prefix: true
after_save :keep_around_commit
after_save :create_ref
def commit
project.commit(sha)
......@@ -29,8 +29,8 @@ class Deployment < ActiveRecord::Base
self == environment.last_deployment
end
def keep_around_commit
project.repository.keep_around(self.sha)
def create_ref
project.repository.create_ref(ref, ref_path)
end
def manual_actions
......@@ -76,4 +76,10 @@ class Deployment < ActiveRecord::Base
where.not(id: self.id).
take
end
private
def ref_path
File.join(environment.ref_path, 'deployments', id.to_s)
end
end
......@@ -47,4 +47,8 @@ class Environment < ActiveRecord::Base
def update_merge_request_metrics?
self.name == "production"
end
def ref_path
"refs/environments/#{Shellwords.shellescape(name)}"
end
end
......@@ -523,9 +523,13 @@ class MergeRequest < ActiveRecord::Base
# `MergeRequestsClosingIssues` model. This is a performance optimization.
# Calculating this information for a number of merge requests requires
# 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)
return if project.has_external_issue_tracker?
transaction do
self.merge_requests_closing_issues.delete_all
closes_issues(current_user).each do |issue|
self.merge_requests_closing_issues.create!(issue: issue)
end
......
......@@ -997,6 +997,10 @@ class Repository
Gitlab::Popen.popen(args, path_to_repo)
end
def create_ref(ref, ref_path)
fetch_ref(path_to_repo, ref, ref_path)
end
def update_branch_with_hooks(current_user, branch)
update_autocrlf_option
......
......@@ -136,6 +136,7 @@ class Service < ActiveRecord::Base
end
def #{arg}=(value)
self.properties ||= {}
updated_properties['#{arg}'] = #{arg} unless #{arg}_changed?
self.properties['#{arg}'] = value
end
......
......@@ -827,6 +827,22 @@ class User < ActiveRecord::Base
todos_pending_count(force: true)
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
def projects_union(min_access_level = nil)
......
......@@ -14,6 +14,8 @@ module Members
if member.request? && member.user != user
notification_service.decline_access_request(member)
end
member
end
end
end
module Members
class DestroyService < BaseService
attr_accessor :member, :current_user
include MembersHelper
def initialize(member, current_user)
@member = member
attr_accessor :source
ALLOWED_SCOPES = %i[members requesters all]
def initialize(source, current_user, params = {})
@source = source
@current_user = current_user
@params = params
end
def execute
unless member && can?(current_user, "destroy_#{member.type.underscore}".to_sym, member)
raise Gitlab::Access::AccessDeniedError
end
def execute(scope = :members)
raise "scope :#{scope} is not allowed!" unless ALLOWED_SCOPES.include?(scope)
member = find_member!(scope)
raise Gitlab::Access::AccessDeniedError unless can_destroy_member?(member)
AuthorizedDestroyService.new(member, current_user).execute
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
......@@ -347,7 +347,7 @@ module SystemNoteService
notes = notes.where(noteable_id: noteable.id)
end
notes_for_mentioner(mentioner, noteable, notes).count > 0
notes_for_mentioner(mentioner, noteable, notes).exists?
end
# Build an Array of lines detailing each commit added in a merge request
......
......@@ -63,6 +63,11 @@
Reply by email
%span.light.pull-right
= boolean_to_icon Gitlab::IncomingEmail.enabled?
%p
Container Registry
%span.light.pull-right
= boolean_to_icon Gitlab.config.registry.enabled
.col-md-4
%h4
Components
......
......@@ -19,6 +19,7 @@
(removed)
&middot; #{time_ago_with_tooltip(todo.created_at)}
= todo_due_date(todo)
.todo-body
.todo-note
......
%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
%button.side-nav-toggle{ type: 'button', "aria-label" => "Toggle global navigation" }
%span.sr-only Toggle navigation
......
......@@ -4,9 +4,9 @@ $('body').addClass('<%= user_application_theme %>')
// Toggle container-fluid class
if ('<%= current_user.layout %>' === 'fluid') {
$('.content-wrapper').find('.container-fluid').removeClass('container-limited')
$('.content-wrapper .container-fluid').removeClass('container-limited')
} else {
$('.content-wrapper').find('.container-fluid').addClass('container-limited')
$('.content-wrapper .container-fluid').addClass('container-limited')
}
// Re-enable the "Save" button
......
- show_whitespace_toggle = local_assigns.fetch(:show_whitespace_toggle, true)
- diff_files = diffs.diff_files
- if diff_view == :parallel
- fluid_layout true
.content-block.oneline-block.files-changed
.inline-parallel-buttons
......
......@@ -4,9 +4,6 @@
- content_for :page_specific_javascripts do
= 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)}
= render "projects/merge_requests/show/mr_title"
......
......@@ -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].
### 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
You can create and delete environments manually in the web interface, but we
......
......@@ -75,9 +75,8 @@ module API
required_attributes! [:user_id]
source = find_source(source_type, params[:id])
access_requester = source.requesters.find_by!(user_id: params[:user_id])
::Members::DestroyService.new(access_requester, current_user).execute
::Members::DestroyService.new(source, current_user, params).
execute(:requesters)
end
end
end
......
......@@ -134,7 +134,7 @@ module API
if member.nil?
{ message: "Access revoked", id: params[:user_id].to_i }
else
::Members::DestroyService.new(member, current_user).execute
::Members::DestroyService.new(source, current_user, params).execute
present member.user, with: Entities::Member, member: member
end
......
......@@ -4,20 +4,18 @@ module API
before { authenticate! }
resource :namespaces do
# Get a namespaces list
#
# Example Request:
# GET /namespaces
desc 'Get a namespaces list' do
success Entities::Namespace
end
params do
optional :search, type: String, desc: "Search query for namespaces"
end
get do
@namespaces = if current_user.admin
Namespace.all
else
current_user.namespaces
end
@namespaces = @namespaces.search(params[:search]) if params[:search].present?
@namespaces = paginate @namespaces
namespaces = current_user.admin ? Namespace.all : current_user.namespaces
namespaces = namespaces.search(params[:search]) if params[:search].present?
present @namespaces, with: Entities::Namespace
present paginate(namespaces), with: Entities::Namespace
end
end
end
......
......@@ -87,10 +87,10 @@ describe Groups::GroupMembersController do
context 'when member is not found' do
before { sign_in(user) }
it 'returns 403' do
it 'returns 404' do
delete :leave, group_id: group
expect(response).to have_http_status(403)
expect(response).to have_http_status(404)
end
end
......
......@@ -135,11 +135,11 @@ describe Projects::ProjectMembersController do
context 'when member is not found' do
before { sign_in(user) }
it 'returns 403' do
it 'returns 404' do
delete :leave, namespace_id: project.namespace,
project_id: project
expect(response).to have_http_status(403)
expect(response).to have_http_status(404)
end
end
......
......@@ -109,6 +109,44 @@ describe SessionsController do
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
let(:another_user) { create(:user) }
......
......@@ -26,7 +26,7 @@ feature 'issuable templates', feature: true, js: true do
scenario 'user selects "bug" template' do
select_template 'bug'
wait_for_ajax
preview_template
preview_template(template_content)
save_changes
end
......@@ -42,6 +42,26 @@ feature 'issuable templates', feature: true, js: true do
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
let(:template_content) { 'this is a test "feature-proposal" template' }
let(:merge_request) { create(:merge_request, :with_diffs, source_project: project) }
......@@ -55,7 +75,7 @@ feature 'issuable templates', feature: true, js: true do
scenario 'user selects "feature-proposal" template' do
select_template 'feature-proposal'
wait_for_ajax
preview_template
preview_template(template_content)
save_changes
end
end
......@@ -82,16 +102,16 @@ feature 'issuable templates', feature: true, js: true do
scenario 'user selects template' do
select_template 'feature-proposal'
wait_for_ajax
preview_template
preview_template(template_content)
save_changes
end
end
end
end
def preview_template
def preview_template(expected_content)
click_link 'Preview'
expect(page).to have_content template_content
expect(page).to have_content expected_content
end
def save_changes
......
......@@ -4,7 +4,7 @@ describe 'Dashboard Todos', feature: true do
let(:user) { create(:user) }
let(:author) { create(:user) }
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
context 'User does not have todos' do
......@@ -28,6 +28,12 @@ describe 'Dashboard Todos', feature: true do
expect(page).to have_selector('.todos-list .todo', count: 1)
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
before do
first('.done-todo').click
......
require 'spec_helper'
describe Mentionable do
include Mentionable
class Example
include Mentionable
def author
nil
attr_accessor :project, :message
attr_mentionable :message
def author
nil
end
end
describe 'references' do
let(:project) { create(:project) }
let(:mentionable) { Example.new }
it 'excludes JIRA references' do
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
......@@ -39,9 +48,8 @@ describe Issue, "Mentionable" do
let(:user) { create(:user) }
def referenced_issues(current_user)
text = "#{private_issue.to_reference(project)} and #{public_issue.to_reference}"
issue.referenced_mentionables(current_user, text)
issue.title = "#{private_issue.to_reference(project)} and #{public_issue.to_reference}"
issue.referenced_mentionables(current_user)
end
context 'when the current user can see the issue' do
......
......@@ -86,6 +86,30 @@ describe MergeRequest, models: true do
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
let(:last_branch_commit) { subject.source_project.repository.commit(subject.source_branch) }
......@@ -522,7 +546,7 @@ describe MergeRequest, models: true do
end
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(:set_mentionable_text) { ->(txt){ subject.description = txt } }
......
......@@ -320,6 +320,16 @@ describe Repository, models: true do
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
before do
repository.send(:cache).expire(:changelog)
......
......@@ -203,6 +203,23 @@ describe Service, models: true do
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
let(:project) { create(:project) }
let!(:service) do
......
......@@ -195,7 +195,7 @@ describe API::AccessRequests, api: true do
end
context 'when authenticated as the access requester' do
it 'returns 200' do
it 'deletes the access requester' do
expect do
delete api("/#{source_type.pluralize}/#{source.id}/access_requests/#{access_requester.id}", access_requester)
......@@ -205,7 +205,7 @@ describe API::AccessRequests, api: true do
end
context 'when authenticated as a master/owner' do
it 'returns 200' do
it 'deletes the access requester' do
expect do
delete api("/#{source_type.pluralize}/#{source.id}/access_requests/#{access_requester.id}", master)
......@@ -213,6 +213,16 @@ describe API::AccessRequests, api: true do
end.to change { source.requesters.count }.by(-1)
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
it 'returns 404' do
expect do
......
......@@ -2,70 +2,111 @@ require 'spec_helper'
describe Members::DestroyService, services: true do
let(:user) { create(:user) }
let(:project) { create(:project) }
let!(:member) { create(:project_member, source: project) }
let(:member_user) { create(:user) }
let(:project) { create(:project, :public) }
let(:group) { create(:group, :public) }
context 'when member is nil' do
before do
project.team << [user, :developer]
shared_examples 'a service raising ActiveRecord::RecordNotFound' do
it 'raises ActiveRecord::RecordNotFound' do
expect { described_class.new(source, user, params).execute }.to raise_error(ActiveRecord::RecordNotFound)
end
end
it 'does not destroy the member' do
expect { destroy_member(nil, user) }.to raise_error(Gitlab::Access::AccessDeniedError)
shared_examples 'a service raising Gitlab::Access::AccessDeniedError' do
it 'raises Gitlab::Access::AccessDeniedError' do
expect { described_class.new(source, user, params).execute }.to raise_error(Gitlab::Access::AccessDeniedError)
end
end
context 'when current user cannot destroy the given member' do
before do
project.team << [user, :developer]
shared_examples 'a service destroying a member' do
it 'destroys the member' do
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
context 'when no member are found' do
let(:params) { { user_id: 42 } }
it 'does not destroy the member' do
expect { destroy_member(member, user) }.to raise_error(Gitlab::Access::AccessDeniedError)
it_behaves_like 'a service raising ActiveRecord::RecordNotFound' do
let(:source) { project }
end
it_behaves_like 'a service raising ActiveRecord::RecordNotFound' do
let(:source) { group }
end
end
context 'when current user can destroy the given member' do
context 'when a member is found' do
before do
project.team << [user, :master]
project.team << [member_user, :developer]
group.add_developer(member_user)
end
let(:params) { { user_id: member_user.id } }
it 'destroys the member' do
destroy_member(member, user)
context 'when current user cannot destroy the given member' do
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
context 'when the given member is a requester' do
context 'when current user can destroy the given member' do
before do
member.update_column(:requested_at, Time.now)
project.team << [user, :master]
group.add_owner(user)
end
it 'calls Member#after_decline_request' do
expect_any_instance_of(NotificationService).to receive(:decline_access_request).with(member)
destroy_member(member, user)
it_behaves_like 'a service destroying a member' do
let(:source) { project }
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(member)
destroy_member(member, member.user)
end
it_behaves_like 'a service destroying a member' do
let(:source) { group }
end
context 'when current user is the member and ' do
it 'does not call Member#after_decline_request' do
expect_any_instance_of(NotificationService).not_to receive(:decline_access_request).with(member)
context 'when given a :id' do
let(:params) { { id: project.members.find_by!(user_id: user.id).id } }
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
def destroy_member(member, user)
Members::DestroyService.new(member, user).execute
end
end
......@@ -38,6 +38,42 @@ describe MergeRequests::MergeService, services: true do
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
let(:merge_request) { create(:merge_request, assignee: user, author: user) }
let(:project) { merge_request.project }
......
......@@ -9,7 +9,7 @@ shared_context 'mentionable context' do
let(:author) { subject.author }
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(:ext_proj) { create(:project, :public) }
......@@ -100,6 +100,7 @@ shared_examples 'an editable mentionable' do
it 'creates new cross-reference notes when the mentionable text is edited' do
subject.save
subject.create_cross_references!
new_text = <<-MSG.strip_heredoc
These references already existed:
......@@ -131,6 +132,7 @@ shared_examples 'an editable mentionable' do
end
# 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|
expect(SystemNoteService).to receive(:cross_reference).
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