Commit 828c218f authored by Valery Sizov's avatar Valery Sizov

Merge branch 'master' of gitlab.com:gitlab-org/gitlab-ee into ce_upstream

parents deae7c98 80a16d49
......@@ -2,6 +2,9 @@ Please view this file on the master branch, on stable branches it's out of date.
v 8.9.0 (unreleased)
- Fix pipeline status when there are no builds in pipeline
v 8.10.0 (unreleased)
v 8.9.0
- Fix Error 500 when using closes_issues API with an external issue tracker
- Add more information into RSS feed for issues (Alexander Matyushentsev)
- Bulk assign/unassign labels to issues.
......
Please view this file on the master branch, on stable branches it's out of date.
v 8.9.0 (unreleased)
v 8.10.0 (unreleased)
v 8.9.1
- Improve Geo documentation. !431
- Fix remote mirror stuck on started issue. !491
- Fix MR creation from forks where target project has approvals enabled. !496
- Fix MR edit where target project has approvals enabled. !496
- Fix vertical alignment of git-hooks page. !499
v 8.9.0
- Fix JenkinsService test button
- Fix nil user handling in UpdateMirrorService
- Allow overriding the number of approvers for a merge request
- Allow LDAP to mark users as external based on their group membership. !432
- Forbid MR authors from approving their own MRs
- Instrument instance methods of Gitlab::InsecureKeyFingerprint class
- Add API endpoint for Merge Request Approvals !449
- Send notification email when merge request is approved
- Distribute RepositoryUpdateMirror jobs in time and add exclusive lease on them by project_id
- [Elastic] Move ES settings to application settings
- Disable mirror flag for projects without import_url
- UpdateMirror service return an error status when no mirror
- Don't reset approvals when rebasing an MR from the UI
- Show flash notice when Git Hooks are updated successfully
- Remove explicit Gitlab::Metrics.action assignments, are already automatic.
- [Elastic] Project members with guest role can't access confidential issues
- Ability to lock file or folder in the repository
- Fix: Git hooks don't fire when committing from the UI
v 8.8.5
- Make sure OAuth routes that we generate for Geo matches with the ones in Rails routes !444
......
......@@ -39,6 +39,9 @@
e.preventDefault()
e.stopImmediatePropagation()
return false
gl.utils.capitalize = (str) ->
return str[0].toUpperCase() + str.slice(1);
jQuery.timefor = (time, suffix, expiredLabel) ->
......
class @PathLocks
@init: (url, path) ->
$('.path-lock').on 'click', ->
$lockBtn = $(this)
currentState = $lockBtn.data('state')
toggleAction = if currentState is 'lock' then 'unlock' else 'lock'
$.post url, {
path: path
}, ->
$lockBtn.text(gl.utils.capitalize(toggleAction))
$lockBtn.data('state', toggleAction)
......@@ -10,7 +10,7 @@ class Admin::GitHooksController < Admin::ApplicationController
@git_hook.update_attributes(git_hook_params.merge(is_sample: true))
if @git_hook.valid?
redirect_to admin_git_hooks_path
redirect_to admin_git_hooks_path, notice: 'Git Hooks updated successfully.'
else
render :index
end
......
......@@ -17,7 +17,7 @@ class Projects::GitHooksController < Projects::ApplicationController
@git_hook.update_attributes(git_hook_params)
if @git_hook.valid?
redirect_to namespace_project_git_hooks_path(@project.namespace, @project)
redirect_to namespace_project_git_hooks_path(@project.namespace, @project), notice: 'Git Hooks updated successfully.'
else
render :index
end
......
......@@ -136,13 +136,17 @@ class Projects::MergeRequestsController < Projects::ApplicationController
def create
@target_branches ||= []
@merge_request = MergeRequests::CreateService.new(project, current_user, merge_request_params).execute
create_params = clamp_approvals_before_merge(merge_request_params)
@merge_request = MergeRequests::CreateService.new(project, current_user, create_params).execute
if @merge_request.valid?
redirect_to(merge_request_path(@merge_request))
else
@source_project = @merge_request.source_project
@target_project = @merge_request.target_project
set_suggested_approvers
render action: "new"
end
end
......@@ -156,7 +160,9 @@ class Projects::MergeRequestsController < Projects::ApplicationController
end
def update
@merge_request = MergeRequests::UpdateService.new(project, current_user, merge_request_params).execute(@merge_request)
update_params = clamp_approvals_before_merge(merge_request_params)
@merge_request = MergeRequests::UpdateService.new(project, current_user, update_params).execute(@merge_request)
if @merge_request.valid?
respond_to do |format|
......@@ -169,6 +175,8 @@ class Projects::MergeRequestsController < Projects::ApplicationController
end
end
else
set_suggested_approvers
render "edit"
end
end
......@@ -385,7 +393,8 @@ class Projects::MergeRequestsController < Projects::ApplicationController
def set_suggested_approvers
if @merge_request.requires_approve?
@suggested_approvers = Gitlab::AuthorityAnalyzer.new(
@merge_request
@merge_request,
current_user
).calculate(@merge_request.approvals_required)
end
end
......@@ -395,10 +404,19 @@ class Projects::MergeRequestsController < Projects::ApplicationController
:title, :assignee_id, :source_project_id, :source_branch,
:target_project_id, :target_branch, :milestone_id, :approver_ids,
:state_event, :description, :task_num, :force_remove_source_branch,
:approvals_before_merge,
label_ids: []
)
end
def clamp_approvals_before_merge(mr_params)
if mr_params[:approvals_before_merge].to_i <= selected_target_project.approvals_before_merge
mr_params.delete(:approvals_before_merge)
end
mr_params
end
def merge_params
params.permit(:should_remove_source_branch, :commit_message)
end
......
class Projects::PathLocksController < Projects::ApplicationController
include PathLocksHelper
# Authorize
before_action :require_non_empty_project
before_action :authorize_push_code!, only: [:toggle]
before_action :check_license
def index
@path_locks = @project.path_locks.page(params[:page])
end
def toggle
path_lock = @project.path_locks.find_by(path: params[:path])
if path_lock
PathLocks::UnlockService.new(project, current_user).execute(path_lock)
else
PathLocks::LockService.new(project, current_user).execute(params[:path])
end
head :ok
rescue PathLocks::UnlockService::AccessDenied, PathLocks::LockService::AccessDenied
return access_denied!
end
def destroy
path_lock = @project.path_locks.find(params[:id])
begin
PathLocks::UnlockService.new(project, current_user).execute(path_lock)
rescue PathLocks::UnlockService::AccessDenied
return access_denied!
end
respond_to do |format|
format.html do
redirect_to namespace_project_locks_path(@project.namespace, @project)
end
format.js
end
end
private
def check_license
unless license_allows_file_locks?
flash[:alert] = 'You need a different license to enable FileLocks feature'
redirect_to admin_license_path
end
end
end
class Projects::RefsController < Projects::ApplicationController
include ExtractsPath
include TreeHelper
include PathLocksHelper
before_action :require_non_empty_project
before_action :validate_ref_id
......@@ -57,16 +58,22 @@ class Projects::RefsController < Projects::ApplicationController
contents.push(*tree.blobs)
contents.push(*tree.submodules)
show_path_locks = license_allows_file_locks? && @project.path_locks.any?
@logs = contents[@offset, @limit].to_a.map do |content|
file = @path ? File.join(@path, content.name) : content.name
last_commit = @repo.last_commit_for_path(@commit.id, file)
path_lock_info = show_path_locks && @project.path_lock_info(file, exact_match: true)
{
file_name: content.name,
commit: last_commit
commit: last_commit,
path_lock_info: path_lock_info
}
end
offset = (@offset + @limit)
offset = @offset + @limit
if contents.size > offset
@more_log_url = logs_file_namespace_project_ref_path(@project.namespace, @project, @ref, @path || '', offset: offset)
end
......
......@@ -295,7 +295,8 @@ class IssuableFinder
end
def weights?
params[:weight].present? && params[:weight] != Issue::WEIGHT_ALL
params[:weight].present? && params[:weight] != Issue::WEIGHT_ALL &&
klass.column_names.include?('weight')
end
def filter_by_no_weight?
......
......@@ -29,7 +29,7 @@ module BlobHelper
if !on_top_of_branch?(project, ref)
button_tag "Edit", class: "btn disabled has-tooltip btn-file-option", title: "You can only edit files when you are on a branch", data: { container: 'body' }
elsif can_edit_blob?(blob, project, ref)
link_to "Edit", edit_path, class: 'btn btn-file-option'
link_to "Edit", edit_path, class: 'btn btn-sm'
elsif can?(current_user, :fork_project, project)
continue_params = {
to: edit_path,
......
module PathLocksHelper
def can_unlock?(path_lock, current_user = @current_user, project = @project)
can?(current_user, :admin_path_locks, project) || path_lock.user == current_user
end
def license_allows_file_locks?
@license_allows_file_locks ||= (::License.current && ::License.current.add_on?('GitLab_FileLocks'))
end
end
......@@ -123,4 +123,20 @@ module TreeHelper
return tree.name
end
end
def lock_file_link(project = @project, path = @path, html_options: {})
return unless license_allows_file_locks?
return if path.blank?
path_lock = project.path_lock_info(path, exact_match: true)
# Check permissions to unlock
return if path_lock && !can_unlock?(path_lock)
# Check permissions to lock
return if !path_lock && !can?(current_user, :push_code, project)
label = path_lock ? 'Unlock' : 'Lock'
html_options = html_options.merge(data: { state: label.downcase })
link_to label, '#', html_options
end
end
......@@ -42,6 +42,13 @@ module Emails
mail_answer_thread(@merge_request, merge_request_thread_options(updated_by_user_id, recipient_id))
end
def approved_merge_request_email(recipient_id, merge_request_id, updated_by_user_id)
setup_merge_request_mail(merge_request_id, recipient_id)
@approved_by_users = @merge_request.approved_by_users.map(&:name)
mail_answer_thread(@merge_request, merge_request_thread_options(updated_by_user_id, recipient_id))
end
private
def setup_merge_request_mail(merge_request_id, recipient_id)
......
......@@ -300,7 +300,8 @@ class Ability
:admin_pages,
:admin_pipeline,
:admin_environment,
:admin_deployment
:admin_deployment,
:admin_path_locks
]
end
......
......@@ -69,7 +69,7 @@ module Elastic
should: [
{ term: { author_id: current_user.id } },
{ term: { assignee_id: current_user.id } },
{ terms: { project_id: current_user.authorized_projects.pluck(:id) } }
{ terms: { project_id: current_user.authorized_projects(Gitlab::Access::REPORTER).pluck(:id) } }
]
}
}
......
......@@ -92,7 +92,7 @@ module Elastic
should: [
{ term: { "issue.author_id" => current_user.id } },
{ term: { "issue.assignee_id" => current_user.id } },
{ terms: { "issue.project_id" => current_user.authorized_projects.pluck(:id) } }
{ terms: { "project_id" => current_user.authorized_projects(Gitlab::Access::REPORTER).pluck(:id) } }
]
}
}
......
......@@ -45,8 +45,6 @@ module Issuable
scope :order_milestone_due_desc, -> { outer_join_milestone.reorder('milestones.due_date IS NULL ASC, milestones.due_date DESC, milestones.id DESC') }
scope :order_milestone_due_asc, -> { outer_join_milestone.reorder('milestones.due_date IS NULL ASC, milestones.due_date ASC, milestones.id ASC') }
scope :without_label, -> { joins("LEFT OUTER JOIN label_links ON label_links.target_type = '#{name}' AND label_links.target_id = #{table_name}.id").where(label_links: { id: nil }) }
scope :order_weight_desc, -> { reorder('weight IS NOT NULL, weight DESC') }
scope :order_weight_asc, -> { reorder('weight ASC') }
scope :left_joins_milestones, -> { joins("LEFT OUTER JOIN milestones ON #{table_name}.milestone_id = milestones.id") }
scope :order_milestone_due_desc, -> { left_joins_milestones.reorder('milestones.due_date IS NULL, milestones.id IS NULL, milestones.due_date DESC') }
......@@ -122,8 +120,6 @@ module Issuable
when 'milestone_due_desc' then order_milestone_due_desc
when 'downvotes_desc' then order_downvotes_desc
when 'upvotes_desc' then order_upvotes_desc
when 'weight_desc' then order_weight_desc
when 'weight_asc' then order_weight_asc
when 'priority' then order_labels_priority(excluded_labels: excluded_labels)
else
order_by(method)
......
......@@ -37,6 +37,8 @@ class Issue < ActiveRecord::Base
scope :order_due_date_asc, -> { reorder('issues.due_date IS NULL, issues.due_date ASC') }
scope :order_due_date_desc, -> { reorder('issues.due_date IS NULL, issues.due_date DESC') }
scope :order_weight_desc, -> { reorder('weight IS NOT NULL, weight DESC') }
scope :order_weight_asc, -> { reorder('weight ASC') }
state_machine :state, initial: :opened do
event :close do
......@@ -93,6 +95,8 @@ class Issue < ActiveRecord::Base
case method.to_s
when 'due_date_asc' then order_due_date_asc
when 'due_date_desc' then order_due_date_desc
when 'weight_desc' then order_weight_desc
when 'weight_asc' then order_weight_asc
else
super
end
......
......@@ -105,6 +105,7 @@ class MergeRequest < ActiveRecord::Base
validates :merge_user, presence: true, if: :merge_when_build_succeeds?
validate :validate_branches, unless: :allow_broken
validate :validate_fork
validate :validate_approvals_before_merge
scope :by_branch, ->(branch_name) { where("(source_branch LIKE :branch) OR (target_branch LIKE :branch)", branch: branch_name) }
scope :cared, ->(user) { where('assignee_id = :user OR author_id = :user', user: user.id) }
......@@ -220,6 +221,22 @@ class MergeRequest < ActiveRecord::Base
end
end
def validate_approvals_before_merge
return true unless approvals_before_merge
return true unless target_project
# Approvals disabled
if target_project.approvals_before_merge == 0
errors.add :validate_approvals_before_merge,
'Approvals disabled for target project'
elsif approvals_before_merge > target_project.approvals_before_merge
true
else
errors.add :validate_approvals_before_merge,
'Number of approvals must be greater than those on target project'
end
end
def update_merge_request_diff
if source_branch_changed? || target_branch_changed?
reload_code
......@@ -475,11 +492,12 @@ class MergeRequest < ActiveRecord::Base
end
def approvers_left
User.where(id: overall_approvers.select(:user_id)).where.not(id: approvals.select(:user_id))
user_ids = overall_approvers.map(&:user_id) - approvals.map(&:user_id)
User.where id: user_ids
end
def approvals_required
target_project.approvals_before_merge
approvals_before_merge || target_project.approvals_before_merge
end
def requires_approve?
......@@ -507,10 +525,8 @@ class MergeRequest < ActiveRecord::Base
end
def can_approve?(user)
return false if user == self.author
approvers_left.include?(user) ||
(any_approver_allowed? && !approved_by?(user))
(any_approver_allowed? && !approved_by?(user))
end
def any_approver_allowed?
......
class PathLock < ActiveRecord::Base
belongs_to :project
belongs_to :user
validates :project, presence: true
validates :user, presence: true
validates :path, presence: true, uniqueness: { scope: [:user, :project] }
end
......@@ -132,6 +132,7 @@ class Project < ActiveRecord::Base
has_many :remote_mirrors, dependent: :destroy
has_many :environments, dependent: :destroy
has_many :deployments, dependent: :destroy
has_many :path_locks, dependent: :destroy
accepts_nested_attributes_for :variables, allow_destroy: true
accepts_nested_attributes_for :remote_mirrors,
......@@ -1208,6 +1209,11 @@ class Project < ActiveRecord::Base
Dir.exist?(public_pages_path)
end
def path_lock_info(path, exact_match: false)
@path_lock_finder ||= Gitlab::PathLocksFinder.new(self)
@path_lock_finder.get_lock_info(path, exact_match: exact_match)
end
def schedule_delete!(user_id, params)
# Queue this task for after the commit, so once we mark pending_delete it will run
run_after_commit { ProjectDestroyWorker.perform_async(id, user_id, params) }
......
......@@ -53,14 +53,13 @@ class JenkinsService < CiService
def test(data)
begin
result = execute(data)
message = result.message || result unless result.nil?
return { success: false, result: message } if result.code != 200
code, message = execute(data)
return { success: false, result: message } if code != 200
rescue StandardError => error
return { success: false, result: error }
end
{ success: true, result: result }
{ success: true, result: message }
end
def auth
......
......@@ -38,7 +38,7 @@ class RemoteMirror < ActiveRecord::Base
scope :enabled, -> { where(enabled: true) }
scope :started, -> { with_update_status(:started) }
scope :stuck, -> { started.where('last_update_at < ?', 1.day.ago) }
scope :stuck, -> { started.where('last_update_at < ? OR (last_update_at IS NULL AND updated_at < ?)', 1.day.ago, 1.day.ago) }
state_machine :update_status, initial: :none do
event :update_start do
......
......@@ -88,6 +88,7 @@ class User < ActiveRecord::Base
has_many :todos, dependent: :destroy
has_many :notification_settings, dependent: :destroy
has_many :award_emoji, as: :awardable, dependent: :destroy
has_many :path_locks, dependent: :destroy
#
# Validations
......
......@@ -8,6 +8,7 @@ module MergeRequests
mark_pending_todos_as_done(merge_request)
if merge_request.approvals_left.zero?
notification_service.approve_mr(merge_request, current_user)
execute_hooks(merge_request, 'approved')
end
end
......
......@@ -27,7 +27,11 @@ module MergeRequests
end
# Clone
output, status = popen(%W(git clone -b #{merge_request.source_branch} -- #{source_project.repository.path_to_repo} #{tree_path}))
output, status = popen(
%W(git clone -b #{merge_request.source_branch} -- #{source_project.repository.path_to_repo} #{tree_path}),
nil,
git_env
)
unless status.zero?
log('Failed to clone repository for rebase:')
......@@ -36,7 +40,11 @@ module MergeRequests
end
# Rebase
output, status = popen(%W(git pull --rebase #{target_project.repository.path_to_repo} #{merge_request.target_branch}), tree_path)
output, status = popen(
%W(git pull --rebase #{target_project.repository.path_to_repo} #{merge_request.target_branch}),
tree_path,
git_env
)
unless status.zero?
log('Failed to rebase branch:')
......@@ -44,8 +52,25 @@ module MergeRequests
return false
end
output, status = popen(
%W(git rev-parse #{merge_request.source_branch}),
tree_path,
git_env
)
unless status.zero?
log('Failed to get SHA of rebased branch:')
log(output)
return false
end
merge_request.update_attributes(rebase_commit_sha: output.chomp)
# Push
output, status = popen(%W(git push -f origin #{merge_request.source_branch}), tree_path)
output, status = popen(
%W(git push -f origin #{merge_request.source_branch}),
tree_path,
git_env
)
unless status.zero?
log('Failed to push rebased branch:')
......@@ -80,5 +105,9 @@ module MergeRequests
def clean_dir
FileUtils.rm_rf(tree_path) if File.exist?(tree_path)
end
def git_env
{ 'GL_ID' => Gitlab::GlId.gl_id(current_user) }
end
end
end
......@@ -83,7 +83,10 @@ module MergeRequests
merge_requests_for_source_branch.each do |merge_request|
target_project = merge_request.target_project
if target_project.approvals_before_merge.nonzero? && target_project.reset_approvals_on_push
if target_project.approvals_before_merge.nonzero? &&
target_project.reset_approvals_on_push &&
merge_request.rebase_commit_sha != @newrev
merge_request.approvals.destroy_all
end
end
......
......@@ -115,6 +115,10 @@ class NotificationService
)
end
def approve_mr(merge_request, current_user)
approve_mr_email(merge_request, merge_request.target_project, current_user)
end
# Notify new user with email after creation
def new_user(user, token = nil)
# Don't email omniauth created users
......@@ -477,6 +481,14 @@ class NotificationService
end
end
def approve_mr_email(merge_request, project, current_user)
recipients = build_recipients(merge_request, project, current_user)
recipients.each do |recipient|
mailer.approved_merge_request_email(recipient.id, merge_request.id, current_user.id).deliver_later
end
end
def build_recipients(target, project, current_user, action: nil, previous_assignee: nil)
recipients = target.participants(current_user)
recipients = add_project_watchers(recipients, project)
......
module PathLocks
class LockService < BaseService
AccessDenied = Class.new(StandardError)
include PathLocksHelper
def execute(path)
raise AccessDenied, 'You have no permissions' unless can?(current_user, :push_code, project)
project.path_locks.create(path: path, user: current_user)
end
end
end
module PathLocks
class UnlockService < BaseService
AccessDenied = Class.new(StandardError)
include PathLocksHelper
def execute(path_lock)
raise AccessDenied, 'You have no permissions' unless can_unlock?(path_lock)
path_lock.destroy
end
end
end
......@@ -4,7 +4,9 @@ module Projects
class UpdateError < Error; end
def execute
return false unless project.mirror?
unless project.mirror?
return error("The project has no mirror to update")
end
unless can?(current_user, :push_code_to_protected_branches, project)
return error("The mirror user is not allowed to push code to all branches on this project.")
......
......@@ -18,7 +18,7 @@ module Projects
push_tags if changed_tags.present?
delete_tags if deleted_tags.present?
rescue Gitlab::Shell::Error => e
rescue => e
errors << e.message.strip
end
......
......@@ -6,7 +6,7 @@
%hr.clearfix
= form_for [:admin, @git_hook], html: { class: 'form-horizontal' } do |f|
= form_for [:admin, @git_hook] do |f|
-if @git_hook.errors.any?
.alert.alert-danger
- @git_hook.errors.full_messages.each do |msg|
......
......@@ -36,7 +36,7 @@
Activity
- if project_nav_tab? :files
= nav_link(controller: %w(tree blob blame edit_tree new_tree find_file commit commits compare repositories tags branches releases network)) do
= nav_link(controller: %w(tree blob blame edit_tree new_tree find_file commit commits compare repositories tags branches releases network path_locks)) do
= link_to project_files_path(@project), title: 'Code', class: 'shortcuts-tree' do
%span
Code
......
%p
= "Merge Request #{@merge_request.to_reference} was approved by #{@approved_by_users.to_sentence}"
= "Merge Request #{@merge_request.to_reference} was approved by #{@approved_by_users.to_sentence}"
Merge Request url: #{namespace_project_merge_request_url(@merge_request.target_project.namespace, @merge_request.target_project, @merge_request)}
= merge_path_description(@merge_request, 'to')
Author: #{@merge_request.author_name}
Assignee: #{@merge_request.assignee_name}
......@@ -16,6 +16,14 @@
- if current_user
.btn-group{ role: "group" }
= lock_file_link(html_options: {class: 'btn btn-sm path-lock'})
= edit_blob_link
= replace_blob_link
= delete_blob_link
- if license_allows_file_locks?
:javascript
PathLocks.init(
'#{toggle_namespace_project_path_locks_path(@project.namespace, @project)}',
'#{@path}'
);
......@@ -25,4 +25,9 @@
= nav_link(controller: [:tags, :releases]) do
= link_to namespace_project_tags_path(@project.namespace, @project) do
Tags
- if license_allows_file_locks?
= nav_link(controller: [:path_locks]) do
= link_to namespace_project_path_locks_path(@project.namespace, @project) do
Locked Files
.fade-right
%li
%div
%span.item-title
= icon('lock')
= path_lock.path
.controls
- if can_unlock?(path_lock)
= link_to namespace_project_path_lock_path(@project.namespace, @project, path_lock), class: 'btn btn-grouped btn-xs btn-remove remove-row has-tooltip', title: "Unlock", method: :delete, data: { confirm: "Are you sure you want to unlock #{path_lock.path}?", container: 'body' }, remote: true do
= icon("trash-o")
locked by #{path_lock.user.name} #{time_ago_with_tooltip(path_lock.created_at)}
- unless @project.path_locks.any?
$('.locks').load(document.URL + ' .nothing-here-block').hide().fadeIn(1000)
- @no_container = true
- page_title "File Locks"
= render "projects/commits/head"
%div{ class: (container_class) }
.top-area
.nav-text
Locks give the ability to lock specific file or folder.
.locks
- if @path_locks.any?
%ul.content-list
= render @path_locks
= paginate @path_locks, theme: 'gitlab'
- else
.nothing-here-block
Repository has no locks.
- @logs.each do |content_data|
- file_name = content_data[:file_name]
- commit = content_data[:commit]
- path_lock_info = content_data[:path_lock_info]
- next unless commit
:plain
......@@ -8,6 +9,14 @@
row.find("td.tree_time_ago").html('#{escape_javascript time_ago_with_tooltip(commit.committed_date)}');
row.find("td.tree_commit").html('#{escape_javascript render("projects/tree/tree_commit_column", commit: commit)}');
- if path_lock_info
:plain
var label = $("<span>")
.attr('title', 'Locked by #{escape_javascript path_lock_info.user.name}')
.attr('data-toggle', 'tooltip')
.addClass('fa fa-lock prepend-left-5');
row.find('td.tree-item-file-name').append(label);
- if @more_log_url
:plain
if($('#tree-slider').length) {
......
......@@ -15,6 +15,7 @@
= link_to @commit.short_id, namespace_project_commit_path(@project.namespace, @project, @commit), class: "monospace"
&ndash;
= truncate(@commit.title, length: 50)
= lock_file_link(html_options: {class: 'pull-right prepend-left-10 path-lock'})
= link_to 'History', namespace_project_commits_path(@project.namespace, @project, @id), class: 'pull-right'
- if @path.present?
......@@ -33,6 +34,14 @@
= render 'projects/blob/upload', title: 'Upload New File', placeholder: 'Upload new file', button_title: 'Upload file', form_path: namespace_project_create_blob_path(@project.namespace, @project, @id), method: :post
= render 'projects/blob/new_dir'
- if license_allows_file_locks?
:javascript
PathLocks.init(
'#{toggle_namespace_project_path_locks_path(@project.namespace, @project)}',
'#{@path}'
);
:javascript
// Load last commit log for each file in tree
$('#tree-slider').waitForImages(function() {
......
......@@ -18,10 +18,11 @@
= sort_title_recently_updated
= link_to page_filter_path(sort: sort_value_oldest_updated) do
= sort_title_oldest_updated
= link_to page_filter_path(sort: sort_value_more_weight) do
= sort_title_more_weight
= link_to page_filter_path(sort: sort_value_less_weight) do
= sort_title_less_weight
- if local_assigns[:type] == :issues
= link_to page_filter_path(sort: sort_value_more_weight) do
= sort_title_more_weight
= link_to page_filter_path(sort: sort_value_less_weight) do
= sort_title_less_weight
= link_to page_filter_path(sort: sort_value_milestone_soon) do
= sort_title_milestone_soon
= link_to page_filter_path(sort: sort_value_milestone_later) do
......
......@@ -26,7 +26,7 @@
.filter-item.inline.labels-filter
= render "shared/issuable/label_dropdown"
- if controller.controller_name == 'issues'
- if local_assigns[:type] == :issues
.filter-item.inline.weight-filter
- if params[:weight]
= hidden_field_tag(:weight, params[:weight])
......@@ -39,7 +39,7 @@
= weight
.pull-right
= render 'shared/sort_dropdown'
= render 'shared/sort_dropdown', type: local_assigns[:type]
- if controller.controller_name == 'issues'
.issues_bulk_update.hide
......
......@@ -113,13 +113,23 @@
- if issuable.is_a?(MergeRequest)
- if @merge_request.requires_approve?
- approvals = issuable.target_project.approvals_before_merge
.form-group
= f.label :approvals_before_merge, class: 'control-label' do
Approvals required
.col-sm-10
= f.number_field :approvals_before_merge, class: 'form-control', value: approvals
.help-block
Number of users who need to approve this merge request before it can be accepted.
If this isn't greater than the project default (#{pluralize(approvals, 'user')}),
then it will be ignored and the project default will be used.
.form-group
= f.label :approver_ids, class: 'control-label' do
Approvers
.col-sm-10
= users_select_tag("merge_request[approver_ids]", multiple: true, class: 'input-large', scope: :all, email_user: true)
.help-block
Merge Request should be approved by these users.
This merge request must be approved by these users.
You can override the project settings by setting your own list of approvers.
.panel.panel-default.prepend-top-10
......
......@@ -746,6 +746,11 @@ Rails.application.routes.draw do
resources :tags, only: [:index, :show, :new, :create, :destroy], constraints: { id: Gitlab::Regex.git_reference_regex } do
resource :release, only: [:edit, :update]
end
resources :path_locks, only: [:index, :destroy] do
collection do
post :toggle
end
end
resources :protected_branches, only: [:index, :create, :update, :destroy], constraints: { id: Gitlab::Regex.git_reference_regex }
resources :variables, only: [:index, :show, :update, :create, :destroy]
......
class CreatePathLocksTable < ActiveRecord::Migration
def change
create_table :path_locks do |t|
t.string :path, null: false, index: true
t.references :project, index: true, foreign_key: true
t.references :user, index: true, foreign_key: true
t.timestamps null: false
end
end
end
class AddApprovalsBeforeMergeToMergeRequests < ActiveRecord::Migration
include Gitlab::Database::MigrationHelpers
def change
add_column :merge_requests, :approvals_before_merge, :integer
end
end
# See http://doc.gitlab.com/ce/development/migration_style_guide.html
# for more information on how to write migrations for GitLab.
class AddRebaseCommitShaToMergeRequests < ActiveRecord::Migration
include Gitlab::Database::MigrationHelpers
# When using the methods "add_concurrent_index" or "add_column_with_default"
# you must disable the use of transactions as these methods can not run in an
# existing transaction. When using "add_concurrent_index" make sure that this
# method is the _only_ method called in the migration, any other changes
# should go in a separate migration. This ensures that upon failure _only_ the
# index creation fails and can be retried or reverted easily.
#
# To disable transactions uncomment the following line and remove these
# comments:
# disable_ddl_transaction!
def change
add_column :merge_requests, :rebase_commit_sha, :string
end
end
......@@ -11,7 +11,7 @@
#
# It's strongly recommended that you check this file into your version control system.
ActiveRecord::Schema.define(version: 20160616084004) do
ActiveRecord::Schema.define(version: 20160621123729) do
# These are extensions that must be enabled in order to support this database
enable_extension "plpgsql"
......@@ -707,6 +707,8 @@ ActiveRecord::Schema.define(version: 20160616084004) do
t.integer "merge_user_id"
t.string "merge_commit_sha"
t.datetime "deleted_at"
t.integer "approvals_before_merge"
t.string "rebase_commit_sha"
end
add_index "merge_requests", ["assignee_id"], name: "index_merge_requests_on_assignee_id", using: :btree
......@@ -863,6 +865,18 @@ ActiveRecord::Schema.define(version: 20160616084004) do
add_index "pages_domains", ["domain"], name: "index_pages_domains_on_domain", unique: true, using: :btree
create_table "path_locks", force: :cascade do |t|
t.string "path", null: false
t.integer "project_id"
t.integer "user_id"
t.datetime "created_at", null: false
t.datetime "updated_at", null: false
end
add_index "path_locks", ["path"], name: "index_path_locks_on_path", using: :btree
add_index "path_locks", ["project_id"], name: "index_path_locks_on_project_id", using: :btree
add_index "path_locks", ["user_id"], name: "index_path_locks_on_user_id", using: :btree
create_table "project_group_links", force: :cascade do |t|
t.integer "project_id", null: false
t.integer "group_id", null: false
......@@ -1233,6 +1247,8 @@ ActiveRecord::Schema.define(version: 20160616084004) do
add_index "web_hooks", ["created_at", "id"], name: "index_web_hooks_on_created_at_and_id", using: :btree
add_index "web_hooks", ["project_id"], name: "index_web_hooks_on_project_id", using: :btree
add_foreign_key "path_locks", "projects"
add_foreign_key "path_locks", "users"
add_foreign_key "remote_mirrors", "projects"
add_foreign_key "u2f_registrations", "users"
end
# LDAP Additions in GitLab EE
This is a continuation of the main [LDAP documentation](ldap.md), detailing LDAP
features specific to GitLab Enterprise Edition.
## User Sync
Once per day, GitLab will run a worker to check and update GitLab
users against LDAP.
The process will execute the following access checks:
1. Ensure the user is still present in LDAP
1. If the LDAP server is Active Directory, ensure the user is active (not
blocked/disabled state). This will only be checked if
`active_directory: true` is set in the LDAP configuration [^1]
The user will be set to `ldap_blocked` state in GitLab if the above conditions
fail. This means the user will not be able to login or push/pull code.
The process will also update the following user information:
1. Email address
1. If `sync_ssh_keys` is set, SSH public keys
1. If Kerberos is enabled, Kerberos identity
> **Note:** The LDAP sync process updates existing users while new users will
be created on first sign in.
## Group Sync
If `group_base` is set in LDAP configuration, a group sync process will run
every hour, on the hour. This allows GitLab group membership to be automatically
updated based on LDAP group members.
The `group_base` configuration should be a base LDAP 'container', such as an
'organization' or 'organizational unit', that contains LDAP groups that should
be available to GitLab. For example, `group_base` could be
`ou=groups,dc=example,dc=com`. In the config file it will look like the
following.
**Omnibus configuration**
Edit `/etc/gitlab/gitlab.rb`:
```ruby
gitlab_rails['ldap_servers'] = YAML.load <<-EOS
main:
# snip...
group_base: ou=groups,dc=example,dc=com
EOS
```
[Reconfigure GitLab][reconfigure] for the changes to take effect.
**Source configuration**
Edit `/home/git/gitlab/config/gitlab.yml`:
```yaml
production:
ldap:
servers:
main:
# snip...
group_base: ou=groups,dc=example,dc=com
```
[Restart GitLab][restart] for the changes to take effect.
---
To take advantage of group sync, group owners or masters will need to create an
LDAP group link in their group **Settings -> LDAP Groups** page. Multiple LDAP
groups can be linked with a single GitLab group. When the link is created, an
access level/role is specified (Guest, Reporter, Developer, Master, or Owner).
## Administrator Sync
As an extension of group sync, you can automatically manage your global GitLab
administrators. Specify a group CN for `admin_group` and all members of the
LDAP group will be given administrator privileges. The configuration will look
like the following.
> **Note:** Administrators will not be synced unless `group_base` is also
specified alongside `admin_group`. Also, only specify the CN of the admin
group, as opposed to the full DN.
**Omnibus configuration**
Edit `/etc/gitlab/gitlab.rb`:
```ruby
gitlab_rails['ldap_servers'] = YAML.load <<-EOS
main:
# snip...
group_base: ou=groups,dc=example,dc=com
admin_group: my_admin_group
EOS
```
[Reconfigure GitLab][reconfigure] for the changes to take effect.
**Source configuration**
Edit `/home/git/gitlab/config/gitlab.yml`:
```yaml
production:
ldap:
servers:
main:
# snip...
group_base: ou=groups,dc=example,dc=com
admin_group: my_admin_group
```
[Restart GitLab][restart] for the changes to take effect.
## Group Sync Technical Details
There is a lot going on with group sync 'under the hood'. This section
outlines what LDAP queries are executed and what behavior you can expect
from group sync.
Group member access will be downgraded from a higher level if their LDAP group
membership changes. For example, if a user has 'Owner' rights in a group and the
next group sync reveals they should only have 'Developer' privileges, their
access will be adjusted accordingly. The only exception is if the user is the
*last* owner in a group. Groups need at least one owner to fulfill
administrative duties.
### Supported LDAP Group Types/Attributes
GitLab supports LDAP groups that use member attributes `member`, `submember`,
`uniquemember`, `memberof` and `memberuid`. This means group sync supports, at
least, LDAP groups with object class `groupOfNames`, `posixGroup`, and
`groupOfUniqueName`. Other object classes should work fine as long as members
are defined as one of the mentioned attributes. This also means GitLab supports
Microsoft Active Directory, Apple Open Directory, Open LDAP, and 389 Server.
Other LDAP servers should work, too.
Active Directory also supports nested groups. Group sync will recursively
resolve membership if `active_directory: true` is set in the configuration file.
### Queries
- Each LDAP group is queried a maximum of one time with base `group_base` and
filter `(cn=<cn_from_group_link>)`.
- If the LDAP group has the `memberuid` attribute, GitLab will execute another
LDAP query per member to obtain each user's full DN. These queries are
executed with base `base`, scope 'base object', and a filter depending on
whether `user_filter` is set. Filter may be `(uid=<uid_from_group>)` or a
joining of `user_filter`.
### Benchmarks
Group sync was written to be as performant as possible. Data is cached, database
queries are optimized, and LDAP queries are minimized. The last benchmark run
revealed the following metrics:
For 20,000 LDAP users, 11,000 LDAP groups and 1,000 GitLab groups with 10
LDAP group links each:
- Initial sync (no existing members assigned in GitLab) took 1.8 hours
- Subsequent syncs (checking membership, no writes) took 15 minutes
These metrics are meant to provide a baseline and performance may vary based on
any number of factors. This was a pretty extreme benchmark and most instances will
not have near this many users or groups. Disk speed, database performance,
network and LDAP server response time will affect these metrics.
## Troubleshooting
If you see `LDAP search error: Referral` in the logs, or when troubleshooting
LDAP Group Sync, this error may indicate a configuration problem. The LDAP
configuration `/etc/gitlab/gitlab.rb` (Omnibus) or `config/gitlab.yml` (source)
is in YAML format and is sensitive to indentation. Check that `group_base` and
`admin_group` configuration keys are indented 2 spaces past the server
identifier. The default identifier is `main` and an example snippet looks like
the following:
```yaml
main: # 'main' is the GitLab 'provider ID' of this LDAP server
label: 'LDAP'
host: 'ldap.example.com'
...
group_base: 'cn=my_group,ou=groups,dc=example,dc=com'
admin_group: 'my_admin_group'
```
[reconfigure]: ../restart_gitlab.md#omnibus-gitlab-reconfigure
[restart]: ../restart_gitlab.md#installations-from-source
[^1]: In Active Directory, a user is marked as disabled/blocked if the user
account control attribute (`userAccountControl:1.2.840.113556.1.4.803`)
has bit 2 set. See https://ctogonewild.com/2009/09/03/bitmask-searches-in-ldap/
for more information.
......@@ -6,6 +6,11 @@ servers, including Microsoft Active Directory, Apple Open Directory, Open LDAP,
and 389 Server. GitLab EE includes enhanced integration, including group
membership syncing.
## GitLab EE
The information on this page is relevent for both GitLab CE and EE. For more
details about EE-specific LDAP features, see [LDAP EE Documentation](ldap-ee.md).
## Security
GitLab assumes that LDAP users are not able to change their LDAP 'mail', 'email'
......@@ -48,6 +53,11 @@ The configuration inside `gitlab_rails['ldap_servers']` below is sensitive to
incorrect indentation. Be sure to retain the indentation given in the example.
Copy/paste can sometimes cause problems.
> **Note:** The `method` value `ssl` corresponds to 'Simple TLS' in the LDAP
library. `tls` corresponds to StartTLS, not to be confused with regular TLS.
Normally, if you specify `ssl` is will be on port 636 while `tls` (StartTLS)
would be on port 389. `plain` also operates on port 389.
**Omnibus configuration**
```ruby
......@@ -130,35 +140,35 @@ main: # 'main' is the GitLab 'provider ID' of this LDAP server
first_name: 'givenName'
last_name: 'sn'
## EE only
# Base where we can search for groups
#
# Ex. ou=groups,dc=gitlab,dc=example
#
group_base: ''
# The CN of a group containing GitLab administrators
#
# Ex. administrators
#
# Note: Not `cn=administrators` or the full DN
#
admin_group: ''
# An array of CNs of groups containing users that should be considered external
#
# Ex. ['interns', 'contractors']
#
# Note: Not `cn=interns` or the full DN
#
external_groups: []
# The LDAP attribute containing a user's public SSH key
#
# Ex. ssh_public_key
#
sync_ssh_keys: false
## EE only
# Base where we can search for groups
#
# Ex. ou=groups,dc=gitlab,dc=example
#
group_base: ''
# The CN of a group containing GitLab administrators
#
# Ex. administrators
#
# Note: Not `cn=administrators` or the full DN
#
admin_group: ''
# An array of CNs of groups containing users that should be considered external
#
# Ex. ['interns', 'contractors']
#
# Note: Not `cn=interns` or the full DN
#
external_groups: []
# The LDAP attribute containing a user's public SSH key
#
# Ex. ssh_public_key
#
sync_ssh_keys: false
# GitLab EE only: add more LDAP servers
# Choose an ID made of a-z and 0-9 . This ID will be stored in the database
......@@ -198,7 +208,7 @@ production:
8.9 and above.
Using the `external_groups` setting will allow you to mark all users belonging
to these groups as [external users](../../permissions/). Group membership is
to these groups as [external users](../../permissions/permissions.md). Group membership is
checked periodically through the `LdapGroupSync` background task.
**Configuration**
......@@ -264,24 +274,24 @@ In other words, if an existing GitLab user wants to enable LDAP sign-in for
themselves, they should check that their GitLab email address matches their
LDAP email address, and then sign into GitLab via their LDAP credentials.
## Limitations
### TLS Client Authentication
## Troubleshooting
Not implemented by `Net::LDAP`.
You should disable anonymous LDAP authentication and enable simple or SASL
authentication. The TLS client authentication setting in your LDAP server cannot
be mandatory and clients cannot be authenticated with the TLS protocol.
### Debug LDAP user filter with ldapsearch
### TLS Server Authentication
This example uses ldapsearch and assumes you are using ActiveDirectory. The
following query returns the login names of the users that will be allowed to
log in to GitLab if you configure your own user_filter.
Not supported by GitLab's configuration options.
When setting `method: ssl`, the underlying authentication method used by
`omniauth-ldap` is `simple_tls`. This method establishes TLS encryption with
the LDAP server before any LDAP-protocol data is exchanged but no validation of
the LDAP server's SSL certificate is performed.
```
ldapsearch -H ldaps://$host:$port -D "$bind_dn" -y bind_dn_password.txt -b "$base" "(&(ObjectClass=User)($user_filter))" sAMAccountName
```
## Troubleshooting
- Variables beginning with a `$` refer to a variable from the LDAP section of
your configuration file.
- Replace ldaps:// with ldap:// if you are using the plain authentication method.
Port `389` is the default `ldap://` port and `636` is the default `ldaps://`
port.
- We are assuming the password for the bind_dn user is in bind_dn_password.txt.
### Invalid credentials when logging in
......
......@@ -68,7 +68,8 @@ Parameters:
"merge_when_build_succeeds": true,
"merge_status": "can_be_merged",
"subscribed" : false,
"user_notes_count": 1
"user_notes_count": 1,
"approvals_before_merge": null
}
]
```
......@@ -132,7 +133,8 @@ Parameters:
"merge_when_build_succeeds": true,
"merge_status": "can_be_merged",
"subscribed" : true,
"user_notes_count": 1
"user_notes_count": 1,
"approvals_before_merge": null
}
```
......@@ -233,6 +235,7 @@ Parameters:
"merge_status": "can_be_merged",
"subscribed" : true,
"user_notes_count": 1,
"approvals_before_merge": null,
"changes": [
{
"old_path": "VERSION",
......@@ -257,15 +260,25 @@ POST /projects/:id/merge_requests
Parameters:
- `id` (required) - The ID of a project
- `source_branch` (required) - The source branch
- `target_branch` (required) - The target branch
- `assignee_id` (optional) - Assignee user ID
- `title` (required) - Title of MR
- `description` (optional) - Description of MR
- `target_project_id` (optional) - The target project (numeric id)
- `labels` (optional) - Labels for MR as a comma-separated list
- `milestone_id` (optional) - Milestone ID
- `id` (required) - The ID of a project
- `source_branch` (required) - The source branch
- `target_branch` (required) - The target branch
- `assignee_id` (optional) - Assignee user ID
- `title` (required) - Title of MR
- `description` (optional) - Description of MR
- `target_project_id` (optional) - The target project (numeric id)
- `labels` (optional) - Labels for MR as a comma-separated list
- `milestone_id` (optional) - Milestone ID
- `approvals_before_merge` (optional) - Number of approvals required before this can be merged (see below)
If `approvals_before_merge` is not provided, it inherits the value from the
target project. If it is provided, then the following conditions must hold in
order for it to take effect:
1. The target project's `approvals_before_merge` must be greater than zero. (A
value of zero disables approvals for that project.)
2. The provided value of `approvals_before_merge` must be greater than the
target project's `approvals_before_merge`.
```json
{
......@@ -312,7 +325,8 @@ Parameters:
"merge_when_build_succeeds": true,
"merge_status": "can_be_merged",
"subscribed" : true,
"user_notes_count": 0
"user_notes_count": 0,
"approvals_before_merge": null
}
```
......@@ -383,7 +397,8 @@ Parameters:
"merge_when_build_succeeds": true,
"merge_status": "can_be_merged",
"subscribed" : true,
"user_notes_count": 1
"user_notes_count": 1,
"approvals_before_merge": null
}
```
......@@ -481,7 +496,8 @@ Parameters:
"merge_when_build_succeeds": true,
"merge_status": "can_be_merged",
"subscribed" : true,
"user_notes_count": 1
"user_notes_count": 1,
"approvals_before_merge": null
}
```
......@@ -535,7 +551,7 @@ GET /projects/:id/merge_requests/:merge_request_id/approvals
>**Note:** This API endpoint is only available on 8.9 EE and above.
If you are allowed to, you can approve a merge request using the following
If you are allowed to, you can approve a merge request using the following
endpoint:
```
......@@ -649,7 +665,8 @@ Parameters:
"merge_when_build_succeeds": true,
"merge_status": "can_be_merged",
"subscribed" : true,
"user_notes_count": 1
"user_notes_count": 1,
"approvals_before_merge": null
}
```
......@@ -715,7 +732,8 @@ Example response when the GitLab issue tracker is used:
"created_at" : "2016-01-04T15:31:51.081Z",
"iid" : 6,
"labels" : [],
"user_notes_count": 1
"user_notes_count": 1,
"approvals_before_merge": null
},
]
```
......
......@@ -7,13 +7,12 @@ We recommend you use with at least GitLab 8.6 EE.
GitLab Geo allows you to replicate your GitLab instance to other geographical
locations as a read-only fully operational version.
<!-- START doctoc generated TOC please keep comment here to allow auto update -->
<!-- DON'T EDIT THIS SECTION, INSTEAD RE-RUN doctoc TO UPDATE -->
- [Overview](#overview)
- [Setup instructions](#setup-instructions)
- [Database Replication](database.md)
- [Configuration](configuration.md)
- [Current limitations](#current-limitations)
- [Disaster Recovery](disaster-recovery.md)
- [Frequently Asked Questions](#frequently-asked-questions)
- [Can I use Geo in a disaster recovery situation?](#can-i-use-geo-in-a-disaster-recovery-situation)
- [What data is replicated to a secondary node?](#what-data-is-replicated-to-a-secondary-node)
......@@ -54,7 +53,7 @@ Geo instances. Follow the steps below in the order that they appear:
1. Install GitLab Enterprise Edition on the server that will serve as the
secondary Geo node
1. [Setup a database replication](./database.md) in `primary <-> secondary (read-only)` topology
1. [Setup a database replication](database.md) in `primary <-> secondary (read-only)` topology
1. [Configure GitLab](configuration.md) and set the primary and secondary nodes
After you set up the database replication and configure the GitLab Geo nodes,
there are a few things to consider:
......@@ -69,6 +68,7 @@ there are a few things to consider:
- You cannot push code to secondary nodes
- Git LFS is not supported yet
- Git Annex is not supported yet
- Primary node has to be online for OAuth login to happen (existing sessions and git are not affected)
## Frequently Asked Questions
......@@ -78,6 +78,9 @@ There are limitations to what we replicate (see Current limitations).
In an extreme data-loss situation you can make a secondary Geo into your
primary, but this is not officially supported yet.
If you still want to proceed, see our step-by-step instructions on how to
manually [promote a secondary node](disaster-recovery.md) into primary.
### What data is replicated to a secondary node?
We currently replicate project repositories and the whole database. This
......
......@@ -56,8 +56,13 @@ primary node (**Admin Area > Geo Nodes**) when adding a new one:
sudo -u git -H ssh-keygen
```
The public key for Omnibus installations will be at `/var/opt/gitlab/.ssh/id_rsa.pub`,
whereas for installation from source it will be at `/home/git/.ssh/id_rsa.pub`.
Remember to add your primary node to the `known_hosts` file of your `git` user.
You can find ssh key files and `know_hosts` at `/var/opt/gitlab/.ssh/` in
Omnibus installations or at `/home/git/.ssh/` when following the source
installation guide.
If for any reason you generate the key using a different name from the default
`id_rsa`, or you want to generate an extra key only for the repository
......@@ -93,11 +98,11 @@ add any secondary servers as well**.
In the following table you can see what all these settings mean:
| Setting | Description |
| ------- | ----------- |
| Primary | This marks a Geo Node as primary. There can be only one primary, make sure that you first add the primary node and then all the others.
| URL | Your instance's full URL, in the same way it is configured in `gitlab.yml` (source based installations) or `/etc/gitlab/gitlab.rb` (omnibus installations). |
|Public Key | The SSH public key of the user that your GitLab instance runs on (unless changed, should be the user `git`). That means that you have to go in each Geo Node separately and create an SSH key pair. See the [SSH key creation](#create-ssh-key-pairs-for-geo-nodes) section.
| Setting | Description |
| --------- | ----------- |
| Primary | This marks a Geo Node as primary. There can be only one primary, make sure that you first add the primary node and then all the others. |
| URL | Your instance's full URL, in the same way it is configured in `gitlab.yml` (source based installations) or `/etc/gitlab/gitlab.rb` (omnibus installations). |
|Public Key | The SSH public key of the user that your GitLab instance runs on (unless changed, should be the user `git`). That means that you have to go in each Geo Node separately and create an SSH key pair. See the [SSH key creation](#create-ssh-key-pairs-for-geo-nodes) section. |
First, add your primary node by providing its full URL and the public SSH key
you created previously. Make sure to check the box 'This is a primary node'
......@@ -147,3 +152,37 @@ gitlab-rake gitlab:shell:setup
# For source installations
sudo -u git -H bundle exec rake gitlab:shell:setup RAILS_ENV=production
```
## Troubleshooting
Setting up Geo requires careful attention to details and sometimes it's easy to
miss a step.
Here is a checklist of questions you should ask to try to detect where you have
to fix (all commands and path locations are for Omnibus installs):
- Is Postgres replication working?
- Are my nodes pointing to the correct database instance?
- You should make sure your primary Geo node points to the instance with
writting permissions.
- Any secondary nodes should point only to read-only instances.
- Can Geo detect my current node correctly?
- Geo uses your defined node from `Admin > Geo` screen, and tries to match
with the value defined in `/etc/gitlab/gitlab.rb` configuration file.
The relevant line looks like: `external_url "http://gitlab.example.com"`.
- To check if node on current machine is correctly detected type:
`sudo gitlab-rails runner "Gitlab::Geo.current_node"`,
expect something like: `#<GeoNode id: 2, schema: "https", host: "gitlab.example.com", port: 443, relative_url_root: "", primary: false, ...>`
- By running the command above, `primary` should be `true` when executed in
the primary node, and `false` on any secondary
- Did I defined the correct SSH Key for the node?
- You must create an SSH Key for `git` user
- This key is the one you have to inform at `Admin > Geo`
- Can primary node communicate with secondary node by HTTP/HTTPS ports?
- Can secondary nodes communicate with primary node by HTTP/HTTPS/SSH ports?
- Can secondary nodes execute a succesfull git clone using git user's own
SSH Key to primary node repository?
> This list is an atempt to document all the moving parts that can go wrong.
We are working into getting all this steps verified automatically in a
rake task in the future. :)
......@@ -220,6 +220,4 @@ When prompted, enter the password you set up for the `gitlab_replicator` user.
## MySQL replication
TODO
[reconfigure gitlab]: ../restart_gitlab.md#omnibus-gitlab-reconfigure
We don't support MySQL replication for GitLab Geo.
# GitLab Geo Disaster Recovery
> **Note:**
This is not officially supported yet, please don't use as your only
Disaster Recovery strategy as you may lose data.
GitLab Geo replicates your database and your Git repositories. We will
support and replicate more data in the future, that will enable you to
fail-over with minimal effort, in a disaster situation.
See [current limitations](README.md#current-limitations)
for more information.
## Promoting a secondary node
We don't provide yet an automated way to promote a node and do fail-over,
but you can do it manually if you have `root` access to the machine.
You must make the changes in the exact specific order:
1. Take down your primary node (or make sure it will not go up during this
process or you may lose data)
2. Wait for any database replication to finish
3. Promote the Postgres in your secondary node as primary
4. Log-in to your secondary node with a user with `sudo` permission
5. Open the interactive rails console: `sudo gitlab-rails console` and execute:
* List your primary node and note down it's id:
```ruby
Gitlab::Geo.primary_node
```
* Turn your primary into a secondary:
```ruby
Gitlab::Geo.primary_node.update(primary: false)
```
* List your secondary nodes and note down the id of the one you want to promote:
```ruby
Gitlab::Geo.secondary_nodes
```
* To promote a node with id `2` execute:
```ruby
GeoNode.find(2).update!(primary: true)
```
* Now you have to cleanup your new promoted node by running:
```ruby
Gitlab::Geo.primary_node.oauth_application.destroy!
Gitlab::Geo.primary_node.system_hook.destroy!
```
* And refresh your old primary node to behave correctly as secondary (assuming id is `1`)
```ruby
GeoNode.find(1).save!
```
* To exit the interactive console, type: `exit`
6. Rsync everything in `/var/opt/gitlab/gitlab-rails/uploads` and
`/var/opt/gitlab/gitlab-rails/shared` from your old node to the new one.
......@@ -4,4 +4,3 @@
- [Requirements](requirements.md)
- [Structure](structure.md)
- [Database MySQL](database_mysql.md)
- [LDAP](ldap.md)
# Link LDAP Groups
You can link LDAP groups with GitLab groups.
It gives you ability to automatically add/remove users from GitLab groups based on LDAP groups membership.
# GitLab LDAP integration
How it works:
1. We retrieve user ldap groups
2. We find corresponding GitLab groups
3. We add user to GitLab groups
4. We remove user from GitLab groups if user has no membership in LDAP groups
In order to use LDAP groups feature:
1. Edit gitlab.yml config LDAP sections.
2. Visit group settings -> LDAP tab
3. Edit LDAP cn and access level for gitlab group
4. Setup LDAP group members
Example of LDAP section from gitlab.yml
```
#
# 2. Auth settings
# ==========================
## LDAP settings
ldap:
enabled: true
host: 'localhost'
base: 'ou=People,dc=gitlab,dc=local'
group_base: 'ou=Groups,dc=gitlab,dc=local'
port: 389
uid: 'uid'
```
# Test whether LDAP group functionality is configured correctly
You need a non-LDAP admin user (such as the default admin@local.host), an LDAP user (e.g. Mary) and an LDAP group to which Mary belongs (e.g. Developers).
1. As the admin, create a new group 'Developers' in GitLab and associate it with the Developers LDAP group at gitlab.example.com/admin/groups/developers/edit .
2. Log in as Mary.
3. Verify that Mary is now a member of the Developers group in GitLab.
If you get an error message when logging in as Mary, double-check your `group_base` setting in `config/gitlab.yml`.
# Debug LDAP user filter with ldapsearch
This example uses [ldapsearch](http://www.openldap.org/software/man.cgi?query=ldapsearch&apropos=0&sektion=0&manpath=OpenLDAP+2.0-Release&format=html) and assumes you are using ActiveDirectory.
The following query returns the login names of the users that will be allowed to log in to GitLab if you configure your own `user_filter`.
```bash
ldapsearch -H ldaps://$host:$port -D "$bind_dn" -y bind_dn_password.txt -b "$base" "(&(ObjectClass=User)($user_filter))" sAMAccountName
```
- `$var` refers to a variable from the `ldap` section of your `config/gitlab.yml` https://gitlab.com/gitlab-org/gitlab-ee/blob/master/config/gitlab.yml.example#L100;
- Replace `ldaps://` with `ldap://` if you are using the `plain` authentication method;
- We are assuming the password for the `bind_dn` user is in `bind_dn_password.txt`.
This document was moved under [`administration/auth/ldap`](../administration/auth/ldap.md).
# File Lock
>**Note:**
This feature was [introduced][ee-440] in GitLab EE 8.9.
---
>**Note:**
This feature needs to have a license with the "File Lock" option enabled. If
you are using Enterprise Edition but you don't see the "Lock" button,
ask your GitLab administrator. Check GitLab's [pricing page] for more information.
GitLab gives you the ability to lock any file or folder in the repository tree
reserving you the right to make changes to that file or folder. **Locking only
works for the default branch you have set in the project's settings** (usually
`master`).
The file locking feature is useful in situations when:
- Multiple people are working on the same file and you won't to avoid merge
conflicts.
- Your repository contains binary files in which situation there is no easy
way to tell the diff between yours and your colleagues' changes.
---
If you lock a file or folder, then you are the only one who can push changes to
the repository where the locked objects are located. Locked folders are locked
recursively.
Locks can be created by any person who has [push access] to the repository; i.e.,
developer and higher level. Any user with master permissions can remove any
lock, no matter who is its author.
## Locking
To lock a file, navigate to the repository tree under the **Code > Files** tab,
pick the file you want to lock and hit the "Lock" button.
![Locking file](img/file_lock.png)
---
To lock an entire folder look for the "Lock" link next to "History".
![Locking folder](img/file_lock_folders.png)
---
After you lock a file or folder, it will appear as locked in the repository
view.
![Repository view](img/file_lock_repository_view.png)
---
To unlock it follow the same procedure or see the following section.
## Viewing/Managing existing locks
To view or manage every existing lock, navigate to the
**Project > Code > Locked Files** section. Only the user that created the lock
and Masters are able to remove the locked objects.
![Locked Files](img/file_lock_list.png)
[ee-440]: https://gitlab.com/gitlab-org/gitlab-ee/merge_requests/440 "File Lock"
[pricing page]: https://about.gitlab.com/pricing
[push access]: ../../permissions/permissions.md
......@@ -8,6 +8,7 @@
- [Importing to GitLab](doc/importing/README.md)
- [Keyboard shortcuts](shortcuts.md)
- [File finder](file_finder.md)
- [File lock](../user/project/file_lock.md)
- [Labels](labels.md)
- [Issue weight](issue_weight.md)
- [Manage large binaries with git annex](git_annex.md)
......
......@@ -33,7 +33,7 @@ independent of changes to the merge request.
### Approvers
At approvers you can define the default set of users that need to approve a
merge request. The author of a merge request cannot approve that merge request.
merge request.
If there are more approvers than required approvals, any subset of these users
can approve the merge request.
......@@ -56,11 +56,12 @@ After configuring Approvals, you will see the following during merge request cre
![Choosing approvers in merge request creation](merge_request_approvals/approvals_mr.png)
You can change the default set of approvers before creating the merge request.
You can't change the amount of required approvals. This ensures that you're
not forced to adjust settings when someone is unavailable for approval, yet
the process is still enforced.
You can change the default set of approvers and the amount of required approvals
before creating the merge request. The amount of required approvals, if changed,
must be greater than the default set at the project level. This ensures that
you're not forced to adjust settings when someone is unavailable for approval,
yet the process is still enforced.
To approve a merge request, simply press the button.
![Merge request approval](merge_request_approvals/2_approvals.png)
![Merge request approval](merge_request_approvals/2_approvals.png)
\ No newline at end of file
......@@ -327,6 +327,13 @@ Feature: Project Merge Requests
And I click link "Close"
Then I should see closed merge request "Bug NS-04"
Scenario: I approve merge request
Given merge request 'Bug NS-04' must be approved
And I click link "Bug NS-04"
And I should not see merge button
When I click link "Approve"
Then I should see approved merge request "Bug NS-04"
Scenario: Reporter can approve merge request
Given I am a "Shop" reporter
And I visit project "Shop" merge requests page
......@@ -336,12 +343,13 @@ Feature: Project Merge Requests
When I click link "Approve"
Then I should see message that merge request can be merged
Scenario: I can not approve Merge request if I am the author
Given merge request 'Bug NS-04' must be approved
Scenario: I approve merge request if I am an approver
Given merge request 'Bug NS-04' must be approved by current user
And I click link "Bug NS-04"
And I should not see merge button
When I should not see Approve button
And I should see message that MR require an approval
And I should see message that MR require an approval from me
When I click link "Approve"
Then I should see approved merge request "Bug NS-04"
Scenario: I can not approve merge request if I am not an approver
Given merge request 'Bug NS-04' must be approved by some user
......
......@@ -211,6 +211,7 @@ module API
merge_request.subscribed?(options[:current_user])
end
expose :user_notes_count
expose :approvals_before_merge
end
class MergeRequestChanges < MergeRequest
......
......@@ -23,8 +23,6 @@ module API
end
post "/allowed" do
Gitlab::Metrics.action = 'Grape#/internal/allowed'
status 200
actor =
......@@ -58,8 +56,6 @@ module API
# Get a ssh key using the fingerprint
#
get "/authorized_keys" do
Gitlab::Metrics.action = 'Grape#/internal/authorized_keys'
fingerprint = params.fetch(:fingerprint) do
Gitlab::InsecureKeyFingerprint.new(params.fetch(:key)).fingerprint
end
......
......@@ -63,15 +63,16 @@ module API
#
# Parameters:
#
# id (required) - The ID of a project - this will be the source of the merge request
# source_branch (required) - The source branch
# target_branch (required) - The target branch
# target_project_id - The target project of the merge request defaults to the :id of the project
# assignee_id - Assignee user ID
# title (required) - Title of MR
# description - Description of MR
# labels (optional) - Labels for MR as a comma-separated list
# milestone_id (optional) - Milestone ID
# id (required) - The ID of a project - this will be the source of the merge request
# source_branch (required) - The source branch
# target_branch (required) - The target branch
# target_project_id (optional) - The target project of the merge request defaults to the :id of the project
# assignee_id (optional) - Assignee user ID
# title (required) - Title of MR
# description (optional) - Description of MR
# labels (optional) - Labels for MR as a comma-separated list
# milestone_id (optional) - Milestone ID
# approvals_before_merge (optional) - Number of approvals required before this can be merged
#
# Example:
# POST /projects/:id/merge_requests
......@@ -79,7 +80,7 @@ module API
post ":id/merge_requests" do
authorize! :create_merge_request, user_project
required_attributes! [:source_branch, :target_branch, :title]
attrs = attributes_for_keys [:source_branch, :target_branch, :assignee_id, :title, :target_project_id, :description, :milestone_id]
attrs = attributes_for_keys [:source_branch, :target_branch, :assignee_id, :title, :target_project_id, :description, :milestone_id, :approvals_before_merge]
# Validate label names in advance
if (errors = validate_label_params(params)).any?
......
......@@ -2,8 +2,9 @@ module Gitlab
class AuthorityAnalyzer
COMMITS_TO_CONSIDER = 5
def initialize(merge_request)
def initialize(merge_request, current_user)
@merge_request = merge_request
@current_user = current_user
@users = Hash.new(0)
end
......@@ -11,7 +12,7 @@ module Gitlab
involved_users
# Picks most active users from hash like: {user1: 2, user2: 6}
@users.sort_by { |user, count| -count }.map(&:first).take(number_of_approvers)
@users.sort_by { |user, count| count }.map(&:first).take(number_of_approvers)
end
private
......@@ -21,9 +22,7 @@ module Gitlab
list_of_involved_files.each do |path|
@repo.commits(@merge_request.target_branch, path: path, limit: COMMITS_TO_CONSIDER).each do |commit|
if commit.author && commit.author != @merge_request.author
@users[commit.author] += 1
end
@users[commit.author] += 1 if commit.author
end
end
end
......
module Gitlab
class GitAccess
include PathLocksHelper
DOWNLOAD_COMMANDS = %w{ git-upload-pack git-upload-archive }
PUSH_COMMANDS = %w{ git-receive-pack }
GIT_ANNEX_COMMANDS = %w{ git-annex-shell }
......@@ -97,7 +99,6 @@ module Gitlab
end
def push_access_check(changes)
if Gitlab::Geo.secondary?
return build_status_object(false, "You can't push code on a secondary GitLab Geo node.")
end
......@@ -186,13 +187,47 @@ module Gitlab
# Return build_status_object(true) if all git hook checks passed successfully
# or build_status_object(false) if any hook fails
git_hook_check(user, project, ref, oldrev, newrev)
result = git_hook_check(user, project, ref, oldrev, newrev)
if result.status && license_allows_file_locks?
result = path_locks_check(user, project, ref, oldrev, newrev)
end
result
end
def forced_push?(oldrev, newrev)
Gitlab::ForcePushCheck.force_push?(project, oldrev, newrev)
end
def path_locks_check(user, project, ref, oldrev, newrev)
unless project.path_locks.any? && newrev && oldrev
return build_status_object(true)
end
# locks protect default branch only
if project.default_branch != branch_name(ref)
return build_status_object(true)
end
commits(newrev, oldrev, project).each do |commit|
next if commit_from_annex_sync?(commit.safe_message)
commit.diffs.each do |diff|
path = diff.new_path || diff.old_path
lock_info = project.path_lock_info(path)
if lock_info && lock_info.user != user
return build_status_object(false, "The path '#{lock_info.path}' is locked by #{lock_info.user.name}")
end
end
end
build_status_object(true)
end
def git_hook_check(user, project, ref, oldrev, newrev)
unless project.git_hook && newrev && oldrev
return build_status_object(true)
......@@ -385,7 +420,8 @@ module Gitlab
end
def old_commit?(commit)
commit.refs(project.repository).any?
# We skip refs/tmp ref because we use it for Web UI commiting
commit.refs(project.repository).reject { |ref| ref.name.start_with?('refs/tmp') }.any?
end
end
end
# The database stores locked paths as following:
# 'app/models/user.rb' or 'app/models'
# To determine that 'app/models/user.rb' is locked we need to generate
# tokens for every requested paths and check every token whether it exist in path locks table or not.
# So for 'app/models/user.rb' path we would need to search next paths:
# 'app', 'app/models' and 'app/models/user.rb'
# This class also implements a memoization for common paths like 'app' 'app/models', 'vendor', etc.
class Gitlab::PathLocksFinder
def initialize(project)
@project = project
@non_locked_paths = []
end
def get_lock_info(path, exact_match: false)
if exact_match
return find_lock(path)
else
tokenize(path).each do |token|
if lock = find_lock(token)
return lock
end
end
false
end
end
private
# This returns hierarchy tokens for path
# app/models/project.rb => ['app', 'app/models', 'app/models/project.rb']
def tokenize(path)
segments = path.split("/")
tokens = []
begin
tokens << segments.join("/")
segments.pop
end until segments.empty?
tokens
end
def find_lock(token)
if @non_locked_paths.include?(token)
return false
end
lock = @project.path_locks.find_by(path: token)
unless lock
@non_locked_paths << token
end
lock
end
end
......@@ -34,6 +34,73 @@ describe Projects::MergeRequestsController do
end
end
describe 'POST #create' do
def create_merge_request(overrides = {})
params = {
namespace_id: project.namespace.to_param,
project_id: project.to_param,
merge_request: {
title: 'Test',
source_branch: 'feature_conflict',
target_branch: 'master',
author: user
}.merge(overrides)
}
post :create, params
end
context 'the approvals_before_merge param' do
before { project.update_attributes(approvals_before_merge: 2) }
let(:created_merge_request) { assigns(:merge_request) }
context 'when it is less than the one in the target project' do
before { create_merge_request(approvals_before_merge: 1) }
it 'sets the param to nil' do
expect(created_merge_request.approvals_before_merge).to eq(nil)
end
it 'creates the merge request' do
expect(created_merge_request).to be_valid
expect(response).to redirect_to(namespace_project_merge_request_path(id: created_merge_request.iid, project_id: project.to_param))
end
end
context 'when it is equal to the one in the target project' do
before { create_merge_request(approvals_before_merge: 2) }
it 'sets the param to nil' do
expect(created_merge_request.approvals_before_merge).to eq(nil)
end
it 'creates the merge request' do
expect(created_merge_request).to be_valid
expect(response).to redirect_to(namespace_project_merge_request_path(id: created_merge_request.iid, project_id: project.to_param))
end
end
context 'when it is greater than the one in the target project' do
before { create_merge_request(approvals_before_merge: 3) }
it 'saves the param in the merge request' do
expect(created_merge_request.approvals_before_merge).to eq(3)
end
it 'creates the merge request' do
expect(created_merge_request).to be_valid
expect(response).to redirect_to(namespace_project_merge_request_path(id: created_merge_request.iid, project_id: project.to_param))
end
end
end
context 'when the merge request is invalid' do
it 'shows the #new form' do
expect(create_merge_request(title: nil)).to render_template(:new)
end
end
end
describe "#show" do
shared_examples "export merge as" do |format|
it "should generally work" do
......@@ -155,6 +222,14 @@ describe Projects::MergeRequestsController do
end
describe 'PUT #update' do
def update_merge_request(params = {})
post :update,
namespace_id: project.namespace.to_param,
project_id: project.to_param,
id: merge_request.iid,
merge_request: params
end
context 'there is no source project' do
let(:project) { create(:project) }
let(:fork_project) { create(:forked_project_with_submodules) }
......@@ -168,18 +243,55 @@ describe Projects::MergeRequestsController do
end
it 'closes MR without errors' do
post :update,
namespace_id: project.namespace.path,
project_id: project.path,
id: merge_request.iid,
merge_request: {
state_event: 'close'
}
update_merge_request(state_event: 'close')
expect(response).to redirect_to([merge_request.target_project.namespace.becomes(Namespace), merge_request.target_project, merge_request])
expect(merge_request.reload.closed?).to be_truthy
end
end
context 'the approvals_before_merge param' do
before { project.update_attributes(approvals_before_merge: 2) }
context 'when it is less than the one in the target project' do
before { update_merge_request(approvals_before_merge: 1) }
it 'sets the param to nil' do
expect(merge_request.reload.approvals_before_merge).to eq(nil)
end
it 'updates the merge request' do
expect(merge_request.reload).to be_valid
expect(response).to redirect_to(namespace_project_merge_request_path(id: merge_request.iid, project_id: project.to_param))
end
end
context 'when it is equal to the one in the target project' do
before { update_merge_request(approvals_before_merge: 2) }
it 'sets the param to nil' do
expect(merge_request.reload.approvals_before_merge).to eq(nil)
end
it 'updates the merge request' do
expect(merge_request.reload).to be_valid
expect(response).to redirect_to(namespace_project_merge_request_path(id: merge_request.iid, project_id: project.to_param))
end
end
context 'when it is greater than the one in the target project' do
before { update_merge_request(approvals_before_merge: 3) }
it 'saves the param in the merge request' do
expect(merge_request.reload.approvals_before_merge).to eq(3)
end
it 'updates the merge request' do
expect(merge_request.reload).to be_valid
expect(response).to redirect_to(namespace_project_merge_request_path(id: merge_request.iid, project_id: project.to_param))
end
end
end
end
describe 'POST #merge' do
......@@ -268,49 +380,6 @@ describe Projects::MergeRequestsController do
end
end
describe 'POST #approve' do
def approve(user)
post :approve, namespace_id: project.namespace.path, project_id: project.path, id: merge_request.iid, user: user
end
context 'when the user is the author of the MR' do
before { merge_request.approvers.create(user: merge_request.author) }
it "returns a 404" do
approve(merge_request.author)
expect(response).to have_http_status(404)
end
end
context 'when the user is not allowed to approve the MR' do
it "returns a 404" do
approve(user)
expect(response).to have_http_status(404)
end
end
context 'when the user is allowed to approve the MR' do
before { merge_request.approvers.create(user: user) }
it 'creates an approval' do
service = double(:approval_service)
expect(MergeRequests::ApprovalService).to receive(:new).with(project, anything).and_return(service)
expect(service).to receive(:execute).with(merge_request)
approve(user)
end
it 'redirects to the MR' do
approve(user)
expect(response).to redirect_to(namespace_project_merge_request_path)
end
end
end
describe "DELETE #destroy" do
it "denies access to users unless they're admin or project owner" do
delete :destroy, namespace_id: project.namespace.path, project_id: project.path, id: merge_request.iid
......
FactoryGirl.define do
factory :path_lock do
project
user
sequence(:path) { |n| "app/model#{n}" }
end
end
require 'spec_helper'
describe 'Issue sorting by Weight', feature: true do
include SortingHelper
let(:project) { create(:project, :public) }
let(:foo) { create(:issue, title: 'foo', project: project) }
let(:bar) { create(:issue, title: 'bar', project: project) }
before do
login_as :user
end
describe 'sorting by weight' do
before do
foo.update(weight: 5)
bar.update(weight: 10)
end
it 'sorts by more weight' do
visit namespace_project_issues_path(project.namespace, project, sort: sort_value_more_weight)
expect(first_issue).to include('bar')
end
it 'sorts by less weight' do
visit namespace_project_issues_path(project.namespace, project, sort: sort_value_less_weight)
expect(first_issue).to include('foo')
end
end
def first_issue
page.all('ul.issues-list > li').first.text
end
end
......@@ -31,6 +31,35 @@ feature 'Create New Merge Request', feature: true, js: true do
expect(page).to have_content 'git checkout -b orphaned-branch origin/orphaned-branch'
end
context 'when approvals are disabled for the target project' do
it 'does not show approval settings' do
visit new_namespace_project_merge_request_path(project.namespace, project, merge_request: { source_branch: 'feature_conflict' })
expect(page).not_to have_content('Approvers')
end
end
context 'when approvals are enabled for the target project' do
before do
project.update_attributes(approvals_before_merge: 1)
visit new_namespace_project_merge_request_path(project.namespace, project, merge_request: { source_branch: 'feature_conflict' })
end
it 'shows approval settings' do
expect(page).to have_content('Approvers')
end
context 'saving the MR' do
it 'shows the saved MR' do
fill_in 'merge_request_title', with: 'Test'
click_button 'Submit merge request'
expect(page).to have_link('Close merge request')
end
end
end
context 'when target project cannot be viewed by the current user' do
it 'does not leak the private project name & namespace' do
private_project = create(:project, :private)
......
......@@ -7,15 +7,24 @@ feature 'Edit Merge Request', feature: true do
before do
project.team << [user, :master]
project.update_attributes(approvals_before_merge: 2)
login_as user
visit edit_namespace_project_merge_request_path(project.namespace, project, merge_request)
end
context 'editing a MR' do
context 'editing a MR that needs approvals' do
it 'form should have class js-quick-submit' do
expect(page).to have_selector('.js-quick-submit')
end
context 'saving the MR' do
it 'shows the saved MR' do
click_button 'Save changes'
expect(page).to have_link('Close merge request')
end
end
end
end
require 'spec_helper'
feature 'Path Locks', feature: true, js: true do
let(:user) { create(:user) }
let(:project) { create(:project, namespace: user.namespace) }
let(:project_tree_path) { namespace_project_tree_path(project.namespace, project, project.repository.root_ref) }
before do
allow_any_instance_of(PathLocksHelper).to receive(:license_allows_file_locks?).and_return(true)
project.team << [user, :master]
login_with(user)
visit project_tree_path
end
scenario 'Locking folders' do
within '.tree-content-holder' do
click_link "encoding"
click_link "Lock"
visit project_tree_path
expect(page).to have_selector('.fa-lock')
end
end
scenario 'Locking files' do
page_tree = find('.tree-content-holder')
within page_tree do
click_link "VERSION"
end
within '.file-actions' do
click_link "Lock"
end
visit project_tree_path
within page_tree do
expect(page).to have_selector('.fa-lock')
end
end
scenario 'Managing of lock list' do
create :path_lock, path: 'encoding', user: user, project: project
click_link "Locked Files"
within '.locks' do
expect(page).to have_content('encoding')
find('.btn-remove').click
expect(page).not_to have_content('encoding')
end
end
end
......@@ -34,6 +34,22 @@ describe IssuesFinder do
expect(issues).to contain_exactly(issue1, issue2, issue3)
end
context 'sort by issues with no weight' do
let(:params) { { weight: Issue::WEIGHT_NONE } }
it 'returns all issues' do
expect(issues).to contain_exactly(issue1, issue2, issue3)
end
end
context 'sort by issues with any weight' do
let(:params) { { weight: Issue::WEIGHT_ANY } }
it 'returns all issues' do
expect(issues).to be_empty
end
end
context 'filtering by assignee ID' do
let(:params) { { assignee_id: user.id } }
......
......@@ -29,5 +29,11 @@ describe MergeRequestsFinder do
merge_requests = MergeRequestsFinder.new(user, params).execute
expect(merge_requests.size).to eq(1)
end
it 'should ignore sorting by weight' do
params = { project_id: project1.id, scope: 'authored', state: 'opened', weight: Issue::WEIGHT_ANY }
merge_requests = MergeRequestsFinder.new(user, params).execute
expect(merge_requests.size).to eq(1)
end
end
end
......@@ -25,4 +25,24 @@ describe TreeHelper do
end
end
end
describe '#lock_file_link' do
let(:path_lock) { create :path_lock }
let(:path) { path_lock.path }
let(:user) { path_lock.user }
let(:project) { path_lock.project }
it "renders unlock link" do
allow(helper).to receive(:can?).and_return(true)
allow(helper).to receive(:license_allows_file_locks?).and_return(true)
expect(helper.lock_file_link(project, path)).to match('Unlock')
end
it "renders lock link" do
allow(helper).to receive(:can?).and_return(true)
allow(helper).to receive(:current_user).and_return(user)
allow(helper).to receive(:license_allows_file_locks?).and_return(true)
expect(helper.lock_file_link(project, 'app/controller')).to match('Lock')
end
end
end
require 'spec_helper'
describe Gitlab::AuthorityAnalyzer, lib: true do
describe '#calculate' do
let(:project) { create(:project) }
let(:author) { create(:user) }
let(:user_a) { create(:user) }
let(:user_b) { create(:user) }
let(:merge_request) { create(:merge_request, target_project: project, source_project: project, author: author) }
let(:files) { [double(:file, deleted_file: true, old_path: 'foo')] }
let(:commits) do
[
double(:commit, author: author),
double(:commit, author: user_a),
double(:commit, author: user_a),
double(:commit, author: user_b),
double(:commit, author: author)
]
end
def calculate_approvers(count)
described_class.new(merge_request).calculate(count)
end
before do
merge_request.compare = double(:compare, diffs: files)
allow(merge_request.target_project.repository).to receive(:commits).and_return(commits)
end
context 'when the MR author is in the top contributors' do
it 'does not include the MR author' do
approvers = calculate_approvers(2)
expect(approvers).not_to include(author)
end
it 'returns the correct number of contributors' do
approvers = calculate_approvers(2)
expect(approvers.length).to eq(2)
end
end
context 'when there are fewer contributors than requested' do
it 'returns the full number of users' do
approvers = calculate_approvers(5)
expect(approvers.length).to eq(2)
end
end
context 'when there are more contributors than requested' do
it 'returns only the top n contributors' do
approvers = calculate_approvers(1)
expect(approvers).to contain_exactly(user_a)
end
end
end
end
......@@ -122,6 +122,18 @@ describe Gitlab::Elastic::ProjectSearchResults, lib: true do
expect(results.issues_count).to eq 3
end
it 'should not list project confidential issues for project members with guest role' do
project.team << [member, :guest]
results = described_class.new(member, project.id, query)
issues = results.objects('issues')
expect(issues).to include issue
expect(issues).not_to include security_issue_1
expect(issues).not_to include security_issue_2
expect(results.issues_count).to eq 1
end
it 'should list all project issues for admin' do
results = described_class.new(admin, project.id, query)
issues = results.objects('issues')
......
......@@ -382,7 +382,8 @@ describe Gitlab::GitAccess, lib: true do
it 'allows githook for new branch with an old bad commit' do
bad_commit = double("Commit", safe_message: 'Some change').as_null_object
allow(bad_commit).to receive(:refs).and_return(['heads/master'])
ref_object = double(name: 'heads/master')
allow(bad_commit).to receive(:refs).and_return([ref_object])
allow_any_instance_of(Repository).to receive(:commits_between).and_return([bad_commit])
project.create_git_hook
......@@ -394,7 +395,8 @@ describe Gitlab::GitAccess, lib: true do
it 'allows githook for any change with an old bad commit' do
bad_commit = double("Commit", safe_message: 'Some change').as_null_object
allow(bad_commit).to receive(:refs).and_return(['heads/master'])
ref_object = double(name: 'heads/master')
allow(bad_commit).to receive(:refs).and_return([ref_object])
allow_any_instance_of(Repository).to receive(:commits_between).and_return([bad_commit])
project.create_git_hook
......@@ -403,6 +405,20 @@ describe Gitlab::GitAccess, lib: true do
# push to new branch, so use a blank old rev and new ref
expect(access.git_hook_check(user, project, 'refs/heads/master', '6f6d7e7ed97bb5f0054f2b1df789b39ca89b6ff9', '570e7b2abdd848b95f2f578043fc23bd6f6fd24d')).to be_allowed
end
it 'does not allow any change from Web UI with bad commit' do
bad_commit = double("Commit", safe_message: 'Some change').as_null_object
# We use tmp ref a a temporary for Web UI commiting
ref_object = double(name: 'refs/tmp')
allow(bad_commit).to receive(:refs).and_return([ref_object])
allow_any_instance_of(Repository).to receive(:commits_between).and_return([bad_commit])
project.create_git_hook
project.git_hook.update(commit_message_regex: "Change some files")
# push to new branch, so use a blank old rev and new ref
expect(access.git_hook_check(user, project, 'refs/heads/master', '6f6d7e7ed97bb5f0054f2b1df789b39ca89b6ff9', '570e7b2abdd848b95f2f578043fc23bd6f6fd24d')).not_to be_allowed
end
end
describe "member_check" do
......
require 'spec_helper'
describe Gitlab::PathLocksFinder, lib: true do
let(:project) { create :empty_project }
let(:user) { create :user }
let(:finder) { Gitlab::PathLocksFinder.new(project) }
it "returns correct lock information" do
lock1 = create :path_lock, project: project, path: 'app'
lock2 = create :path_lock, project: project, path: 'lib/gitlab/repo.rb'
expect(finder.get_lock_info('app')).to eq(lock1)
expect(finder.get_lock_info('app/models/project.rb')).to eq(lock1)
expect(finder.get_lock_info('lib')).to be_falsey
expect(finder.get_lock_info('lib/gitlab/repo.rb')).to eq(lock2)
end
end
......@@ -366,6 +366,46 @@ describe Notify do
end
end
describe 'that are approved' do
let(:last_approver) { create(:user) }
subject { Notify.approved_merge_request_email(recipient.id, merge_request.id, last_approver.id) }
before do
merge_request.approvals.create(user: merge_request.assignee)
merge_request.approvals.create(user: last_approver)
end
it_behaves_like 'a multiple recipients email'
it_behaves_like 'an answer to an existing thread with reply-by-email enabled' do
let(:model) { merge_request }
end
it_behaves_like 'it should show Gmail Actions View Merge request link'
it_behaves_like 'an unsubscribeable thread'
it 'is sent as the last approver' do
sender = subject.header[:from].addrs[0]
expect(sender.display_name).to eq(last_approver.name)
expect(sender.address).to eq(gitlab_sender)
end
it 'has the correct subject' do
is_expected.to have_subject /#{merge_request.title} \(#{merge_request.to_reference}\)/
end
it 'contains the new status' do
is_expected.to have_body_text /approved/i
end
it 'contains a link to the merge request' do
is_expected.to have_body_text /#{namespace_project_merge_request_path project.namespace, project, merge_request}/
end
it 'contains the names of all of the approvers' do
is_expected.to have_body_text /#{merge_request.assignee.name}/
is_expected.to have_body_text /#{last_approver.name}/
end
end
describe 'that are merged' do
subject { Notify.merged_merge_request_email(recipient.id, merge_request.id, merge_author.id) }
......
......@@ -78,5 +78,39 @@ describe Note, elastic: true do
expect(Note.elastic_search('term', options: options).total_count).to eq(1)
end
it "return notes with matching content for project members" do
user = create :user
issue = create :issue, :confidential, author: user
member = create(:user)
issue.project.team << [member, :developer]
create :note, note: 'bla-bla term', project: issue.project, noteable: issue
create :note, project: issue.project, noteable: issue
Note.__elasticsearch__.refresh_index!
options = { project_ids: [issue.project.id], current_user: member }
expect(Note.elastic_search('term', options: options).total_count).to eq(1)
end
it "does not return notes with matching content for project members with guest role" do
user = create :user
issue = create :issue, :confidential, author: user
member = create(:user)
issue.project.team << [member, :guest]
create :note, note: 'bla-bla term', project: issue.project, noteable: issue
create :note, project: issue.project, noteable: issue
Note.__elasticsearch__.refresh_index!
options = { project_ids: [issue.project.id], current_user: member }
expect(Note.elastic_search('term', options: options).total_count).to eq(0)
end
end
end
......@@ -223,7 +223,7 @@ describe MergeRequest, models: true do
end
end
describe "#approvers_left" do
describe "approvers_left" do
let(:merge_request) {create :merge_request}
it "returns correct value" do
......@@ -238,63 +238,20 @@ describe MergeRequest, models: true do
end
describe "#approvals_required" do
let(:merge_request) {create :merge_request}
let(:merge_request) { build(:merge_request) }
before { merge_request.target_project.update_attributes(approvals_before_merge: 3) }
it "takes approvals_before_merge" do
merge_request.target_project.update(approvals_before_merge: 2)
context "when the MR has approvals_before_merge set" do
before { merge_request.update_attributes(approvals_before_merge: 1) }
expect(merge_request.approvals_required).to eq 2
end
end
describe "#can_approve?" do
let(:author) { create(:user) }
let(:user) { create(:user) }
let(:merge_request) { create(:merge_request, author: author) }
context "when the user is the MR author" do
it "returns false" do
expect(merge_request.can_approve?(author)).to eq(false)
it "uses the approvals_before_merge from the MR" do
expect(merge_request.approvals_required).to eq(1)
end
end
context "when the user is not the MR author" do
context "when the user is in the approvers list" do
before { merge_request.approvers.create(user: user) }
context "when the user has not already approved the MR" do
it "returns true" do
expect(merge_request.can_approve?(user)).to eq(true)
end
end
context "when the user has already approved the MR" do
before { merge_request.approvals.create(user: user) }
it "returns false" do
expect(merge_request.can_approve?(user)).to eq(false)
end
end
end
context "when the user is not in the approvers list" do
context "when anyone is allowed to approve the MR" do
before { merge_request.target_project.update_attributes(approvals_before_merge: 1) }
context "when the user has not already approved the MR" do
it "returns true" do
expect(merge_request.can_approve?(user)).to eq(true)
end
end
context "when the user has already approved the MR" do
before { merge_request.approvals.create(user: user) }
it "returns false" do
expect(merge_request.can_approve?(user)).to eq(false)
end
end
end
context "when the MR doesn't have approvals_before_merge set" do
it "takes approvals_before_merge from the target project" do
expect(merge_request.approvals_required).to eq(3)
end
end
end
......
require 'spec_helper'
describe PathLock, models: true do
let(:path_lock) { create(:path_lock) }
it { is_expected.to belong_to(:project) }
it { is_expected.to belong_to(:user) }
it { is_expected.to validate_presence_of(:user) }
it { is_expected.to validate_presence_of(:project) }
it { is_expected.to validate_presence_of(:path) }
it { is_expected.to validate_uniqueness_of(:path).scoped_to(:project_id, :user_id) }
end
......@@ -25,6 +25,16 @@ describe JenkinsService do
it { is_expected.to have_one :service_hook }
end
let(:project) { create(:project) }
let(:jenkins_params) do
{
active: true,
project: project,
properties: {
jenkins_url: 'http://jenkins.example.com/',
project_name: 'my_project'
}
}
end
describe 'username validation' do
before do
......@@ -74,19 +84,26 @@ describe JenkinsService do
end
end
describe '#test' do
it 'returns the right status' do
user = create(:user, username: 'username')
project = create(:project, name: 'project')
push_sample_data = Gitlab::PushDataBuilder.build_sample(project, user)
jenkins_service = described_class.create(jenkins_params)
stub_request(:post, jenkins_service.hook_url)
result = jenkins_service.test(push_sample_data)
expect(result).to eq({ success: true, result: '' })
end
end
describe '#execute' do
it 'adds default web hook headers to the request' do
user = create(:user, username: 'username')
project = create(:project, name: 'project')
push_sample_data = Gitlab::PushDataBuilder.build_sample(project, user)
jenkins_service = described_class.create(
active: true,
project: project,
properties: {
jenkins_url: 'http://jenkins.example.com/',
project_name: 'my_project'
}
)
jenkins_service = described_class.create(jenkins_params)
stub_request(:post, jenkins_service.hook_url)
jenkins_service.execute(push_sample_data)
......
......@@ -32,6 +32,7 @@ describe Project, models: true do
it { is_expected.to have_many(:environments).dependent(:destroy) }
it { is_expected.to have_many(:deployments).dependent(:destroy) }
it { is_expected.to have_many(:todos).dependent(:destroy) }
it { is_expected.to have_many(:path_locks).dependent(:destroy) }
end
describe 'modules' do
......@@ -1095,4 +1096,19 @@ describe Project, models: true do
end
end
end
describe '#path_lock_info' do
let(:project) { create :empty_project }
let(:path_lock) { create :path_lock, project: project }
let(:path) { path_lock.path }
it 'returns path_lock' do
expect(project.path_lock_info(path)).to eq(path_lock)
end
it 'returns nil' do
expect(project.path_lock_info('app/controllers')).to be_falsey
end
end
end
......@@ -23,19 +23,19 @@ describe RemoteMirror do
describe 'encrypting credentials' do
context 'when setting URL for a first time' do
it 'should store the URL without credentials' do
mirror = create_mirror_with_url('http://foo:bar@test.com')
mirror = create_mirror(url: 'http://foo:bar@test.com')
expect(mirror.read_attribute(:url)).to eq('http://test.com')
end
it 'should store the credentials on a separate field' do
mirror = create_mirror_with_url('http://foo:bar@test.com')
mirror = create_mirror(url: 'http://foo:bar@test.com')
expect(mirror.credentials).to eq({ user: 'foo', password: 'bar' })
end
it 'should handle credentials with large content' do
mirror = create_mirror_with_url('http://bxnhm8dote33ct932r3xavslj81wxmr7o8yux8do10oozckkif:9ne7fuvjn40qjt35dgt8v86q9m9g9essryxj76sumg2ccl2fg26c0krtz2gzfpyq4hf22h328uhq6npuiq6h53tpagtsj7vsrz75@test.com')
mirror = create_mirror(url: 'http://bxnhm8dote33ct932r3xavslj81wxmr7o8yux8do10oozckkif:9ne7fuvjn40qjt35dgt8v86q9m9g9essryxj76sumg2ccl2fg26c0krtz2gzfpyq4hf22h328uhq6npuiq6h53tpagtsj7vsrz75@test.com')
expect(mirror.credentials).to eq({
user: 'bxnhm8dote33ct932r3xavslj81wxmr7o8yux8do10oozckkif',
......@@ -46,7 +46,7 @@ describe RemoteMirror do
context 'when updating the URL' do
it 'should allow a new URL without credentials' do
mirror = create_mirror_with_url('http://foo:bar@test.com')
mirror = create_mirror(url: 'http://foo:bar@test.com')
mirror.update_attribute(:url, 'http://test.com')
......@@ -55,7 +55,7 @@ describe RemoteMirror do
end
it 'should allow a new URL with credentials' do
mirror = create_mirror_with_url('http://test.com')
mirror = create_mirror(url: 'http://test.com')
mirror.update_attribute(:url, 'http://foo:bar@test.com')
......@@ -64,7 +64,7 @@ describe RemoteMirror do
end
it 'should update the remote config if credentials changed' do
mirror = create_mirror_with_url('http://foo:bar@test.com')
mirror = create_mirror(url: 'http://foo:bar@test.com')
repo = mirror.project.repository
mirror.update_attribute(:url, 'http://foo:baz@test.com')
......@@ -77,7 +77,7 @@ describe RemoteMirror do
describe '#safe_url' do
context 'when URL contains credentials' do
it 'should mask the credentials' do
mirror = create_mirror_with_url('http://foo:bar@test.com')
mirror = create_mirror(url: 'http://foo:bar@test.com')
expect(mirror.safe_url).to eq('http://*****:*****@test.com')
end
......@@ -85,15 +85,26 @@ describe RemoteMirror do
context 'when URL does not contain credentials' do
it 'should show the full URL' do
mirror = create_mirror_with_url('http://test.com')
mirror = create_mirror(url: 'http://test.com')
expect(mirror.safe_url).to eq('http://test.com')
end
end
end
def create_mirror_with_url(url)
context 'stuck mirrors' do
it 'includes mirrors stuck in started with no last_update_at set' do
mirror = create_mirror(url: 'http://cantbeblank',
update_status: 'started',
last_update_at: nil,
updated_at: 25.hours.ago)
expect(RemoteMirror.stuck.last).to eq(mirror)
end
end
def create_mirror(params)
project = FactoryGirl.create(:project)
project.remote_mirrors.create!(url: url)
project.remote_mirrors.create!(params)
end
end
......@@ -31,6 +31,7 @@ describe User, models: true do
it { is_expected.to have_many(:spam_logs).dependent(:destroy) }
it { is_expected.to have_many(:todos).dependent(:destroy) }
it { is_expected.to have_many(:award_emoji).dependent(:destroy) }
it { is_expected.to have_many(:path_locks).dependent(:destroy) }
end
describe 'validations' do
......
......@@ -353,6 +353,67 @@ describe API::API, api: true do
expect(response.status).to eq(201)
end
end
context 'the approvals_before_merge param' do
def create_merge_request(approvals_before_merge)
post api("/projects/#{project.id}/merge_requests", user),
title: 'Test merge_request',
source_branch: 'feature_conflict',
target_branch: 'master',
author: user,
labels: 'label, label2',
milestone_id: milestone.id,
approvals_before_merge: approvals_before_merge
end
context 'when the target project has approvals_before_merge set to zero' do
before do
project.update_attributes(approvals_before_merge: 0)
create_merge_request(1)
end
it 'returns a 400' do
expect(response).to have_http_status(400)
end
it 'includes the error in the response' do
expect(json_response['message']['validate_approvals_before_merge']).not_to be_empty
end
end
context 'when the target project has a non-zero approvals_before_merge' do
context 'when the approvals_before_merge param is less than or equal to the value in the target project' do
before do
project.update_attributes(approvals_before_merge: 1)
create_merge_request(1)
end
it 'returns a 400' do
expect(response).to have_http_status(400)
end
it 'includes the error in the response' do
expect(json_response['message']['validate_approvals_before_merge']).not_to be_empty
end
end
context 'when the approvals_before_merge param is greater than the value in the target project' do
before do
project.update_attributes(approvals_before_merge: 1)
create_merge_request(2)
end
it 'returns a created status' do
expect(response).to have_http_status(201)
end
it 'sets approvals_before_merge of the newly-created MR' do
expect(json_response['approvals_before_merge']).to eq(2)
end
end
end
end
end
describe "DELETE /projects/:id/merge_requests/:merge_request_id" do
......@@ -634,24 +695,14 @@ describe API::API, api: true do
end
describe 'POST :id/merge_requests/:merge_request_id/approve' do
context 'when the user is an allowed approver' do
it 'approves the merge request' do
project.update_attribute(:approvals_before_merge, 2)
post api("/projects/#{project.id}/merge_requests/#{merge_request.id}/approve", admin)
expect(response.status).to eq(201)
expect(json_response['approvals_left']).to eq(1)
expect(json_response['approved_by'][0]['user']['username']).to eq(admin.username)
end
end
it 'approves the merge request' do
project.update_attribute(:approvals_before_merge, 2)
context 'when the user is the MR author' do
it 'returns a not authorised response' do
post api("/projects/#{project.id}/merge_requests/#{merge_request.id}/approve", user)
post api("/projects/#{project.id}/merge_requests/#{merge_request.id}/approve", user)
expect(response.status).to eq(401)
end
expect(response.status).to eq(201)
expect(json_response['approvals_left']).to eq(1)
expect(json_response['approved_by'][0]['user']['username']).to eq(user.username)
end
end
......
......@@ -47,15 +47,36 @@ describe MergeRequests::ApprovalService, services: true do
service.execute(merge_request)
end
it 'does not send an email' do
expect(merge_request).to receive(:approvals_left).and_return(5)
expect(service).not_to receive(:notification_service)
service.execute(merge_request)
end
end
context 'with required approvals' do
it 'fires a webhook' do
let(:notification_service) { NotificationService.new }
before do
expect(merge_request).to receive(:approvals_left).and_return(0)
allow(service).to receive(:notification_service).and_return(notification_service)
allow(service).to receive(:execute_hooks)
allow(notification_service).to receive(:approve_mr)
end
it 'fires a webhook' do
expect(service).to receive(:execute_hooks).with(merge_request, 'approved')
service.execute(merge_request)
end
it 'sends an email' do
expect(notification_service).to receive(:approve_mr).with(merge_request, user)
service.execute(merge_request)
end
end
end
end
......
......@@ -26,6 +26,11 @@ describe MergeRequests::RebaseService do
target_branch_sha = merge_request.target_project.repository.commit(merge_request.target_branch).sha
expect(parent_sha).to eq(target_branch_sha)
end
it 'records the new SHA on the merge request' do
head_sha = merge_request.source_project.repository.commit(merge_request.source_branch).sha
expect(merge_request.reload.rebase_commit_sha).to eq(head_sha)
end
end
end
end
......@@ -173,24 +173,59 @@ describe MergeRequests::RefreshService, services: true do
end
context 'resetting approvals if they are enabled' do
it "does not reset approvals if approvals_before_merge is disabled" do
@project.update(approvals_before_merge: 0)
refresh_service = service.new(@project, @user)
allow(refresh_service).to receive(:execute_hooks)
refresh_service.execute(@oldrev, @newrev, 'refs/heads/master')
reload_mrs
context 'when approvals_before_merge is disabled' do
before do
@project.update(approvals_before_merge: 0)
refresh_service = service.new(@project, @user)
allow(refresh_service).to receive(:execute_hooks)
refresh_service.execute(@oldrev, @newrev, 'refs/heads/master')
reload_mrs
end
it 'does not reset approvals' do
expect(@merge_request.approvals).not_to be_empty
end
end
expect(@merge_request.approvals).not_to be_empty
context 'when approvals_before_merge is disabled' do
before do
@project.update(reset_approvals_on_push: false)
refresh_service = service.new(@project, @user)
allow(refresh_service).to receive(:execute_hooks)
refresh_service.execute(@oldrev, @newrev, 'refs/heads/master')
reload_mrs
end
it 'does not reset approvals' do
expect(@merge_request.approvals).not_to be_empty
end
end
it "does not reset approvals if reset_approvals_on_push is disabled" do
@project.update(reset_approvals_on_push: false)
refresh_service = service.new(@project, @user)
allow(refresh_service).to receive(:execute_hooks)
refresh_service.execute(@oldrev, @newrev, 'refs/heads/master')
reload_mrs
context 'when the rebase_commit_sha on the MR matches the pushed SHA' do
before do
@merge_request.update(rebase_commit_sha: @newrev)
refresh_service = service.new(@project, @user)
allow(refresh_service).to receive(:execute_hooks)
refresh_service.execute(@oldrev, @newrev, 'refs/heads/master')
reload_mrs
end
it 'does not reset approvals' do
expect(@merge_request.approvals).not_to be_empty
end
end
expect(@merge_request.approvals).not_to be_empty
context 'when there are approvals to be reset' do
before do
refresh_service = service.new(@project, @user)
allow(refresh_service).to receive(:execute_hooks)
refresh_service.execute(@oldrev, @newrev, 'refs/heads/master')
reload_mrs
end
it 'resets the approvals' do
expect(@merge_request.approvals).to be_empty
end
end
end
......
require 'spec_helper'
describe PathLocks::LockService, services: true do
let(:current_user) { create(:user) }
let(:project) { create(:empty_project) }
let(:path) { 'app/models' }
it 'locks path' do
allow_any_instance_of(described_class).to receive(:can?).and_return(true)
described_class.new(project, current_user).execute(path)
expect(project.path_locks.find_by(path: path)).to be_truthy
end
it 'raises exception if user has no permissions' do
expect do
described_class.new(project, current_user).execute(path)
end.to raise_exception(PathLocks::LockService::AccessDenied)
end
end
require 'spec_helper'
describe PathLocks::UnlockService, services: true do
let(:path_lock) { create :path_lock }
let(:current_user) { path_lock.user }
let(:project) { path_lock.project }
let(:path) { path_lock.path }
it 'unlocks path' do
allow_any_instance_of(described_class).to receive(:can?).and_return(true)
described_class.new(project, current_user).execute(path_lock)
expect(project.path_locks.find_by(path: path)).to be_falsey
end
it 'raises exception if user has no permissions' do
user = create :user
expect do
described_class.new(project, user).execute(path_lock)
end.to raise_exception(PathLocks::UnlockService::AccessDenied)
end
end
......@@ -73,6 +73,18 @@ describe Projects::UpdateMirrorService do
expect(result[:status]).to eq(:error)
end
end
describe "when is no mirror" do
let(:project) { build_stubbed(:project) }
it "fails" do
expect(project.mirror?).to eq(false)
result = described_class.new(project, build_stubbed(:user)).execute
expect(result[:status]).to eq(:error)
end
end
end
def stub_fetch_mirror(project, repository: project.repository)
......
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