Commit e85f4697 authored by Douwe Maan's avatar Douwe Maan

Merge branch 'bvl-allow-maintainer-to-push' into 'master'

Allow maintainers to push forks of a project for branches that have open MRs

Closes #22292

See merge request gitlab-org/gitlab-ce!17395
parents 67185097 cfa9d1ed
<script>
export default {
name: 'MRWidgetMaintainerEdit',
props: {
maintainerEditAllowed: {
type: Boolean,
default: false,
required: false,
},
},
};
</script>
<template>
<section class="mr-info-list mr-maintainer-edit">
<p v-if="maintainerEditAllowed">
{{ s__("mrWidget|Allows edits from maintainers") }}
</p>
</section>
</template>
...@@ -15,6 +15,7 @@ export { default as WidgetHeader } from './components/mr_widget_header.vue'; ...@@ -15,6 +15,7 @@ export { default as WidgetHeader } from './components/mr_widget_header.vue';
export { default as WidgetMergeHelp } from './components/mr_widget_merge_help.vue'; export { default as WidgetMergeHelp } from './components/mr_widget_merge_help.vue';
export { default as WidgetPipeline } from './components/mr_widget_pipeline.vue'; export { default as WidgetPipeline } from './components/mr_widget_pipeline.vue';
export { default as WidgetDeployment } from './components/mr_widget_deployment'; export { default as WidgetDeployment } from './components/mr_widget_deployment';
export { default as WidgetMaintainerEdit } from './components/mr_widget_maintainer_edit.vue';
export { default as WidgetRelatedLinks } from './components/mr_widget_related_links.vue'; export { default as WidgetRelatedLinks } from './components/mr_widget_related_links.vue';
export { default as MergedState } from './components/states/mr_widget_merged.vue'; export { default as MergedState } from './components/states/mr_widget_merged.vue';
export { default as FailedToMerge } from './components/states/mr_widget_failed_to_merge.vue'; export { default as FailedToMerge } from './components/states/mr_widget_failed_to_merge.vue';
......
...@@ -6,6 +6,7 @@ import { ...@@ -6,6 +6,7 @@ import {
WidgetMergeHelp, WidgetMergeHelp,
WidgetPipeline, WidgetPipeline,
WidgetDeployment, WidgetDeployment,
WidgetMaintainerEdit,
WidgetRelatedLinks, WidgetRelatedLinks,
MergedState, MergedState,
ClosedState, ClosedState,
...@@ -211,6 +212,7 @@ export default { ...@@ -211,6 +212,7 @@ export default {
'mr-widget-merge-help': WidgetMergeHelp, 'mr-widget-merge-help': WidgetMergeHelp,
'mr-widget-pipeline': WidgetPipeline, 'mr-widget-pipeline': WidgetPipeline,
'mr-widget-deployment': WidgetDeployment, 'mr-widget-deployment': WidgetDeployment,
'mr-widget-maintainer-edit': WidgetMaintainerEdit,
'mr-widget-related-links': WidgetRelatedLinks, 'mr-widget-related-links': WidgetRelatedLinks,
'mr-widget-merged': MergedState, 'mr-widget-merged': MergedState,
'mr-widget-closed': ClosedState, 'mr-widget-closed': ClosedState,
...@@ -251,11 +253,12 @@ export default { ...@@ -251,11 +253,12 @@ export default {
:is="componentName" :is="componentName"
:mr="mr" :mr="mr"
:service="service" /> :service="service" />
<mr-widget-maintainer-edit
:maintainerEditAllowed="mr.maintainerEditAllowed" />
<mr-widget-related-links <mr-widget-related-links
v-if="shouldRenderRelatedLinks" v-if="shouldRenderRelatedLinks"
:state="mr.state" :state="mr.state"
:related-links="mr.relatedLinks" :related-links="mr.relatedLinks" />
/>
</div> </div>
<div <div
class="mr-widget-footer" class="mr-widget-footer"
......
...@@ -76,6 +76,7 @@ export default class MergeRequestStore { ...@@ -76,6 +76,7 @@ export default class MergeRequestStore {
this.canBeMerged = data.can_be_merged || false; this.canBeMerged = data.can_be_merged || false;
this.isMergeAllowed = data.mergeable || false; this.isMergeAllowed = data.mergeable || false;
this.mergeOngoing = data.merge_ongoing; this.mergeOngoing = data.merge_ongoing;
this.maintainerEditAllowed = data.allow_maintainer_to_push;
// Cherry-pick and Revert actions related // Cherry-pick and Revert actions related
this.canCherryPickInCurrentMR = currentUser.can_cherry_pick_on_current_merge_request || false; this.canCherryPickInCurrentMR = currentUser.can_cherry_pick_on_current_merge_request || false;
......
...@@ -453,7 +453,8 @@ ...@@ -453,7 +453,8 @@
} }
} }
.mr-links { .mr-links,
.mr-maintainer-edit {
padding-left: $status-icon-size + $status-icon-margin; padding-left: $status-icon-size + $status-icon-margin;
} }
......
...@@ -4,7 +4,7 @@ module CreatesCommit ...@@ -4,7 +4,7 @@ module CreatesCommit
# rubocop:disable Gitlab/ModuleWithInstanceVariables # rubocop:disable Gitlab/ModuleWithInstanceVariables
def create_commit(service, success_path:, failure_path:, failure_view: nil, success_notice: nil) def create_commit(service, success_path:, failure_path:, failure_view: nil, success_notice: nil)
if can?(current_user, :push_code, @project) if user_access(@project).can_push_to_branch?(branch_name_or_ref)
@project_to_commit_into = @project @project_to_commit_into = @project
@branch_name ||= @ref @branch_name ||= @ref
else else
...@@ -50,7 +50,7 @@ module CreatesCommit ...@@ -50,7 +50,7 @@ module CreatesCommit
# rubocop:enable Gitlab/ModuleWithInstanceVariables # rubocop:enable Gitlab/ModuleWithInstanceVariables
def authorize_edit_tree! def authorize_edit_tree!
return if can_collaborate_with_project? return if can_collaborate_with_project?(project, ref: branch_name_or_ref)
access_denied! access_denied!
end end
...@@ -123,4 +123,8 @@ module CreatesCommit ...@@ -123,4 +123,8 @@ module CreatesCommit
params[:create_merge_request].present? && params[:create_merge_request].present? &&
(different_project? || @start_branch != @branch_name) # rubocop:disable Gitlab/ModuleWithInstanceVariables (different_project? || @start_branch != @branch_name) # rubocop:disable Gitlab/ModuleWithInstanceVariables
end end
def branch_name_or_ref
@branch_name || @ref # rubocop:disable Gitlab/ModuleWithInstanceVariables
end
end end
...@@ -6,7 +6,7 @@ class Projects::ApplicationController < ApplicationController ...@@ -6,7 +6,7 @@ class Projects::ApplicationController < ApplicationController
before_action :repository before_action :repository
layout 'project' layout 'project'
helper_method :repository, :can_collaborate_with_project? helper_method :repository, :can_collaborate_with_project?, :user_access
private private
...@@ -31,11 +31,12 @@ class Projects::ApplicationController < ApplicationController ...@@ -31,11 +31,12 @@ class Projects::ApplicationController < ApplicationController
@repository ||= project.repository @repository ||= project.repository
end end
def can_collaborate_with_project?(project = nil) def can_collaborate_with_project?(project = nil, ref: nil)
project ||= @project project ||= @project
can?(current_user, :push_code, project) || can?(current_user, :push_code, project) ||
(current_user && current_user.already_forked?(project)) (current_user && current_user.already_forked?(project)) ||
user_access(project).can_push_to_branch?(ref)
end end
def authorize_action!(action) def authorize_action!(action)
...@@ -90,4 +91,9 @@ class Projects::ApplicationController < ApplicationController ...@@ -90,4 +91,9 @@ class Projects::ApplicationController < ApplicationController
def check_issues_available! def check_issues_available!
return render_404 unless @project.feature_available?(:issues, current_user) return render_404 unless @project.feature_available?(:issues, current_user)
end end
def user_access(project)
@user_access ||= {}
@user_access[project] ||= Gitlab::UserAccess.new(current_user, project: project)
end
end end
...@@ -9,8 +9,12 @@ class Projects::BlobController < Projects::ApplicationController ...@@ -9,8 +9,12 @@ class Projects::BlobController < Projects::ApplicationController
before_action :require_non_empty_project, except: [:new, :create] before_action :require_non_empty_project, except: [:new, :create]
before_action :authorize_download_code! before_action :authorize_download_code!
before_action :authorize_edit_tree!, only: [:new, :create, :update, :destroy]
# We need to assign the blob vars before `authorize_edit_tree!` so we can
# validate access to a specific ref.
before_action :assign_blob_vars before_action :assign_blob_vars
before_action :authorize_edit_tree!, only: [:new, :create, :update, :destroy]
before_action :commit, except: [:new, :create] before_action :commit, except: [:new, :create]
before_action :blob, except: [:new, :create] before_action :blob, except: [:new, :create]
before_action :require_branch_head, only: [:edit, :update] before_action :require_branch_head, only: [:edit, :update]
...@@ -46,7 +50,7 @@ class Projects::BlobController < Projects::ApplicationController ...@@ -46,7 +50,7 @@ class Projects::BlobController < Projects::ApplicationController
end end
def edit def edit
if can_collaborate_with_project? if can_collaborate_with_project?(project, ref: @ref)
blob.load_all_data! blob.load_all_data!
else else
redirect_to action: 'show' redirect_to action: 'show'
......
...@@ -15,6 +15,7 @@ class Projects::MergeRequests::ApplicationController < Projects::ApplicationCont ...@@ -15,6 +15,7 @@ class Projects::MergeRequests::ApplicationController < Projects::ApplicationCont
def merge_request_params_attributes def merge_request_params_attributes
[ [
:allow_maintainer_to_push,
:assignee_id, :assignee_id,
:description, :description,
:force_remove_source_branch, :force_remove_source_branch,
......
...@@ -125,6 +125,19 @@ module MergeRequestsHelper ...@@ -125,6 +125,19 @@ module MergeRequestsHelper
link_to(url[merge_request.project, merge_request], data: data_attrs, &block) link_to(url[merge_request.project, merge_request], data: data_attrs, &block)
end end
def allow_maintainer_push_unavailable_reason(merge_request)
return if merge_request.can_allow_maintainer_to_push?(current_user)
minimum_visibility = [merge_request.target_project.visibility_level,
merge_request.source_project.visibility_level].min
if minimum_visibility < Gitlab::VisibilityLevel::INTERNAL
_('Not available for private projects')
elsif ProtectedBranch.protected?(merge_request.source_project, merge_request.source_branch)
_('Not available for protected branches')
end
end
def merge_params_ee(merge_request) def merge_params_ee(merge_request)
{} {}
end end
......
...@@ -49,15 +49,13 @@ module TreeHelper ...@@ -49,15 +49,13 @@ module TreeHelper
return false unless on_top_of_branch?(project, ref) return false unless on_top_of_branch?(project, ref)
can_collaborate_with_project?(project) can_collaborate_with_project?(project, ref: ref)
end end
def tree_edit_branch(project = @project, ref = @ref) def tree_edit_branch(project = @project, ref = @ref)
return unless can_edit_tree?(project, ref) return unless can_edit_tree?(project, ref)
project = project.present(current_user: current_user) if user_access(project).can_push_to_branch?(ref)
if project.can_current_user_push_to_branch?(ref)
ref ref
else else
project = tree_edit_project(project) project = tree_edit_project(project)
...@@ -88,7 +86,16 @@ module TreeHelper ...@@ -88,7 +86,16 @@ module TreeHelper
end end
def commit_in_fork_help def commit_in_fork_help
"A new branch will be created in your fork and a new merge request will be started." _("A new branch will be created in your fork and a new merge request will be started.")
end
def commit_in_single_accessible_branch
branch_name = html_escape(selected_branch)
message = _("Your changes can be committed to %{branch_name} because a merge "\
"request is open.") % { branch_name: "<strong>#{branch_name}</strong>" }
message.html_safe
end end
def path_breadcrumbs(max_links = 6) def path_breadcrumbs(max_links = 6)
...@@ -125,4 +132,8 @@ module TreeHelper ...@@ -125,4 +132,8 @@ module TreeHelper
return tree.name return tree.name
end end
end end
def selected_branch
@branch_name || tree_edit_branch
end
end end
...@@ -865,7 +865,7 @@ class MergeRequest < ActiveRecord::Base ...@@ -865,7 +865,7 @@ class MergeRequest < ActiveRecord::Base
def can_be_merged_by?(user) def can_be_merged_by?(user)
access = ::Gitlab::UserAccess.new(user, project: project) access = ::Gitlab::UserAccess.new(user, project: project)
access.can_push_to_branch?(target_branch) || access.can_merge_to_branch?(target_branch) access.can_update_branch?(target_branch)
end end
def can_be_merged_via_command_line_by?(user) def can_be_merged_via_command_line_by?(user)
...@@ -1087,4 +1087,22 @@ class MergeRequest < ActiveRecord::Base ...@@ -1087,4 +1087,22 @@ class MergeRequest < ActiveRecord::Base
project.merge_requests.merged.where(author_id: author_id).empty? project.merge_requests.merged.where(author_id: author_id).empty?
end end
def allow_maintainer_to_push
maintainer_push_possible? && super
end
alias_method :allow_maintainer_to_push?, :allow_maintainer_to_push
def maintainer_push_possible?
source_project.present? && for_fork? &&
target_project.visibility_level > Gitlab::VisibilityLevel::PRIVATE &&
source_project.visibility_level > Gitlab::VisibilityLevel::PRIVATE &&
!ProtectedBranch.protected?(source_project, source_branch)
end
def can_allow_maintainer_to_push?(user)
maintainer_push_possible? &&
Ability.allowed?(user, :push_code, source_project)
end
end end
...@@ -150,6 +150,7 @@ class Project < ActiveRecord::Base ...@@ -150,6 +150,7 @@ class Project < ActiveRecord::Base
# Merge Requests for target project should be removed with it # Merge Requests for target project should be removed with it
has_many :merge_requests, foreign_key: 'target_project_id' has_many :merge_requests, foreign_key: 'target_project_id'
has_many :source_of_merge_requests, foreign_key: 'source_project_id', class_name: 'MergeRequest'
has_many :issues has_many :issues
has_many :labels, class_name: 'ProjectLabel' has_many :labels, class_name: 'ProjectLabel'
has_many :services has_many :services
...@@ -1799,6 +1800,33 @@ class Project < ActiveRecord::Base ...@@ -1799,6 +1800,33 @@ class Project < ActiveRecord::Base
Badge.where("id IN (#{union.to_sql})") # rubocop:disable GitlabSecurity/SqlInjection Badge.where("id IN (#{union.to_sql})") # rubocop:disable GitlabSecurity/SqlInjection
end end
def merge_requests_allowing_push_to_user(user)
return MergeRequest.none unless user
developer_access_exists = user.project_authorizations
.where('access_level >= ? ', Gitlab::Access::DEVELOPER)
.where('project_authorizations.project_id = merge_requests.target_project_id')
.limit(1)
.select(1)
source_of_merge_requests.opened
.where(allow_maintainer_to_push: true)
.where('EXISTS (?)', developer_access_exists)
end
def branch_allows_maintainer_push?(user, branch_name)
return false unless user
cache_key = "user:#{user.id}:#{branch_name}:branch_allows_push"
memoized_results = strong_memoize(:branch_allows_maintainer_push) do
Hash.new do |result, cache_key|
result[cache_key] = fetch_branch_allows_maintainer_push?(user, branch_name)
end
end
memoized_results[cache_key]
end
private private
def storage def storage
...@@ -1921,4 +1949,22 @@ class Project < ActiveRecord::Base ...@@ -1921,4 +1949,22 @@ class Project < ActiveRecord::Base
raise ex raise ex
end end
def fetch_branch_allows_maintainer_push?(user, branch_name)
check_access = -> do
merge_request = source_of_merge_requests.opened
.where(allow_maintainer_to_push: true)
.find_by(source_branch: branch_name)
merge_request&.can_be_merged_by?(user)
end
if RequestStore.active?
RequestStore.fetch("project-#{id}:branch-#{branch_name}:user-#{user.id}:branch_allows_maintainer_push") do
check_access.call
end
else
check_access.call
end
end
end end
...@@ -61,6 +61,11 @@ class ProjectPolicy < BasePolicy ...@@ -61,6 +61,11 @@ class ProjectPolicy < BasePolicy
desc "Project has request access enabled" desc "Project has request access enabled"
condition(:request_access_enabled, scope: :subject) { project.request_access_enabled } condition(:request_access_enabled, scope: :subject) { project.request_access_enabled }
desc "Has merge requests allowing pushes to user"
condition(:has_merge_requests_allowing_pushes, scope: :subject) do
project.merge_requests_allowing_push_to_user(user).any?
end
features = %w[ features = %w[
merge_requests merge_requests
issues issues
...@@ -291,6 +296,15 @@ class ProjectPolicy < BasePolicy ...@@ -291,6 +296,15 @@ class ProjectPolicy < BasePolicy
prevent :read_issue prevent :read_issue
end end
# These rules are included to allow maintainers of projects to push to certain
# to run pipelines for the branches they have access to.
rule { can?(:public_access) & has_merge_requests_allowing_pushes }.policy do
enable :create_build
enable :update_build
enable :create_pipeline
enable :update_pipeline
end
private private
def team_member? def team_member?
......
...@@ -78,7 +78,7 @@ class MergeRequestPresenter < Gitlab::View::Presenter::Delegated ...@@ -78,7 +78,7 @@ class MergeRequestPresenter < Gitlab::View::Presenter::Delegated
end end
def rebase_path def rebase_path
if !rebase_in_progress? && should_be_rebased? && user_can_push_to_source_branch? if !rebase_in_progress? && should_be_rebased? && can_push_to_source_branch?
rebase_project_merge_request_path(project, merge_request) rebase_project_merge_request_path(project, merge_request)
end end
end end
...@@ -160,7 +160,11 @@ class MergeRequestPresenter < Gitlab::View::Presenter::Delegated ...@@ -160,7 +160,11 @@ class MergeRequestPresenter < Gitlab::View::Presenter::Delegated
end end
def can_push_to_source_branch? def can_push_to_source_branch?
source_branch_exists? && user_can_push_to_source_branch? return false unless source_branch_exists?
!!::Gitlab::UserAccess
.new(current_user, project: source_project)
.can_push_to_branch?(source_branch)
end end
private private
...@@ -191,17 +195,10 @@ class MergeRequestPresenter < Gitlab::View::Presenter::Delegated ...@@ -191,17 +195,10 @@ class MergeRequestPresenter < Gitlab::View::Presenter::Delegated
end.sort.to_sentence end.sort.to_sentence
end end
def user_can_push_to_source_branch?
return false unless source_branch_exists?
::Gitlab::UserAccess
.new(current_user, project: source_project)
.can_push_to_branch?(source_branch)
end
def user_can_collaborate_with_project? def user_can_collaborate_with_project?
can?(current_user, :push_code, project) || can?(current_user, :push_code, project) ||
(current_user && current_user.already_forked?(project)) (current_user && current_user.already_forked?(project)) ||
can_push_to_source_branch?
end end
def user_can_fork_project? def user_can_fork_project?
......
...@@ -11,6 +11,7 @@ class MergeRequestWidgetEntity < IssuableEntity ...@@ -11,6 +11,7 @@ class MergeRequestWidgetEntity < IssuableEntity
expose :source_project_id expose :source_project_id
expose :target_branch expose :target_branch
expose :target_project_id expose :target_project_id
expose :allow_maintainer_to_push
expose :should_be_rebased?, as: :should_be_rebased expose :should_be_rebased?, as: :should_be_rebased
expose :ff_only_enabled do |merge_request| expose :ff_only_enabled do |merge_request|
...@@ -29,6 +30,7 @@ class MergeRequestWidgetEntity < IssuableEntity ...@@ -29,6 +30,7 @@ class MergeRequestWidgetEntity < IssuableEntity
expose :can_push_to_source_branch do |merge_request| expose :can_push_to_source_branch do |merge_request|
presenter(merge_request).can_push_to_source_branch? presenter(merge_request).can_push_to_source_branch?
end end
expose :rebase_path do |merge_request| expose :rebase_path do |merge_request|
presenter(merge_request).rebase_path presenter(merge_request).rebase_path
end end
...@@ -136,8 +138,8 @@ class MergeRequestWidgetEntity < IssuableEntity ...@@ -136,8 +138,8 @@ class MergeRequestWidgetEntity < IssuableEntity
end end
expose :new_blob_path do |merge_request| expose :new_blob_path do |merge_request|
if can?(current_user, :push_code, merge_request.project) if presenter(merge_request).can_push_to_source_branch?
project_new_blob_path(merge_request.project, merge_request.source_branch) project_new_blob_path(merge_request.source_project, merge_request.source_branch)
end end
end end
......
...@@ -81,7 +81,7 @@ module Ci ...@@ -81,7 +81,7 @@ module Ci
end end
def related_merge_requests def related_merge_requests
MergeRequest.opened.where(source_project: pipeline.project, source_branch: pipeline.ref) pipeline.project.source_of_merge_requests.opened.where(source_branch: pipeline.ref)
end end
end end
end end
...@@ -35,6 +35,14 @@ module MergeRequests ...@@ -35,6 +35,14 @@ module MergeRequests
end end
end end
def filter_params(merge_request)
super
unless merge_request.can_allow_maintainer_to_push?(current_user)
params.delete(:allow_maintainer_to_push)
end
end
def merge_request_metrics_service(merge_request) def merge_request_metrics_service(merge_request)
MergeRequestMetricsService.new(merge_request.metrics) MergeRequestMetricsService.new(merge_request.metrics)
end end
......
...@@ -6,6 +6,7 @@ module MergeRequests ...@@ -6,6 +6,7 @@ module MergeRequests
@params_issue_iid = params.delete(:issue_iid) @params_issue_iid = params.delete(:issue_iid)
self.merge_request = MergeRequest.new(params) self.merge_request = MergeRequest.new(params)
merge_request.author = current_user
merge_request.compare_commits = [] merge_request.compare_commits = []
merge_request.source_project = find_source_project merge_request.source_project = find_source_project
merge_request.target_project = find_target_project merge_request.target_project = find_target_project
......
...@@ -3,6 +3,4 @@ ...@@ -3,6 +3,4 @@
= link_to 'Cancel', cancel_path, = link_to 'Cancel', cancel_path,
class: 'btn btn-cancel', data: {confirm: leave_edit_message} class: 'btn btn-cancel', data: {confirm: leave_edit_message}
- unless can?(current_user, :push_code, @project) = render 'shared/projects/edit_information'
.inline.prepend-left-10
= commit_in_fork_help
...@@ -17,6 +17,4 @@ ...@@ -17,6 +17,4 @@
= submit_tag _("Create directory"), class: 'btn btn-create' = submit_tag _("Create directory"), class: 'btn btn-create'
= link_to "Cancel", '#', class: "btn btn-cancel", "data-dismiss" => "modal" = link_to "Cancel", '#', class: "btn btn-cancel", "data-dismiss" => "modal"
- unless can?(current_user, :push_code, @project) = render 'shared/projects/edit_information'
.inline.prepend-left-10
= commit_in_fork_help
...@@ -24,6 +24,4 @@ ...@@ -24,6 +24,4 @@
= button_title = button_title
= link_to _("Cancel"), '#', class: "btn btn-cancel", "data-dismiss" => "modal" = link_to _("Cancel"), '#', class: "btn btn-cancel", "data-dismiss" => "modal"
- unless can?(current_user, :push_code, @project) = render 'shared/projects/edit_information'
.inline.prepend-left-10
= commit_in_fork_help
...@@ -35,6 +35,4 @@ ...@@ -35,6 +35,4 @@
= submit_tag label, class: 'btn btn-create' = submit_tag label, class: 'btn btn-create'
= link_to _("Cancel"), '#', class: "btn btn-cancel", "data-dismiss" => "modal" = link_to _("Cancel"), '#', class: "btn btn-cancel", "data-dismiss" => "modal"
- unless can?(current_user, :push_code, @project) = render 'shared/projects/edit_information'
.inline.prepend-left-10
= commit_in_fork_help
- project = @project.present(current_user: current_user)
- branch_name = selected_branch
= render 'shared/commit_message_container', placeholder: placeholder = render 'shared/commit_message_container', placeholder: placeholder
- if @project.empty_repo? - if @project.empty_repo?
...@@ -7,12 +10,14 @@ ...@@ -7,12 +10,14 @@
.form-group.branch .form-group.branch
= label_tag 'branch_name', _('Target Branch'), class: 'control-label' = label_tag 'branch_name', _('Target Branch'), class: 'control-label'
.col-sm-10 .col-sm-10
= text_field_tag 'branch_name', @branch_name || tree_edit_branch, required: true, class: "form-control js-branch-name ref-name" = text_field_tag 'branch_name', branch_name, required: true, class: "form-control js-branch-name ref-name"
.js-create-merge-request-container .js-create-merge-request-container
= render 'shared/new_merge_request_checkbox' = render 'shared/new_merge_request_checkbox'
- elsif project.can_current_user_push_to_branch?(branch_name)
= hidden_field_tag 'branch_name', branch_name
- else - else
= hidden_field_tag 'branch_name', @branch_name || tree_edit_branch = hidden_field_tag 'branch_name', branch_name
= hidden_field_tag 'create_merge_request', 1 = hidden_field_tag 'create_merge_request', 1
= hidden_field_tag 'original_branch', @ref, class: 'js-original-branch' = hidden_field_tag 'original_branch', @ref, class: 'js-original-branch'
...@@ -33,6 +33,8 @@ ...@@ -33,6 +33,8 @@
= render 'shared/issuable/form/merge_params', issuable: issuable = render 'shared/issuable/form/merge_params', issuable: issuable
= render 'shared/issuable/form/contribution', issuable: issuable, form: form
- if @merge_request_to_resolve_discussions_of - if @merge_request_to_resolve_discussions_of
.form-group .form-group
.col-sm-10.col-sm-offset-2 .col-sm-10.col-sm-offset-2
......
- issuable = local_assigns.fetch(:issuable)
- form = local_assigns.fetch(:form)
- return unless issuable.is_a?(MergeRequest)
- return unless issuable.for_fork?
- return unless can?(current_user, :push_code, issuable.source_project)
%hr
.form-group
.control-label
= _('Contribution')
.col-sm-10
.checkbox
= form.label :allow_maintainer_to_push do
= form.check_box :allow_maintainer_to_push, disabled: !issuable.can_allow_maintainer_to_push?(current_user)
= _('Allow edits from maintainers')
= link_to 'About this feature', help_page_path('user/project/merge_requests/maintainer_access')
.help-block
= allow_maintainer_push_unavailable_reason(issuable)
- unless can?(current_user, :push_code, @project)
.inline.prepend-left-10
- if @project.branch_allows_maintainer_push?(current_user, selected_branch)
= commit_in_single_accessible_branch
- else
= commit_in_fork_help
---
title: Allow maintainers to push to forks of their projects when a merge request is open
merge_request: 17395
author:
type: added
# See http://doc.gitlab.com/ce/development/migration_style_guide.html
# for more information on how to write migrations for GitLab.
class AddAllowMaintainerToPushToMergeRequests < ActiveRecord::Migration
include Gitlab::Database::MigrationHelpers
DOWNTIME = false
disable_ddl_transaction!
def up
add_column :merge_requests, :allow_maintainer_to_push, :boolean
end
def down
remove_column :merge_requests, :allow_maintainer_to_push
end
end
...@@ -1145,6 +1145,7 @@ ActiveRecord::Schema.define(version: 20180307012445) do ...@@ -1145,6 +1145,7 @@ ActiveRecord::Schema.define(version: 20180307012445) do
t.boolean "discussion_locked" t.boolean "discussion_locked"
t.integer "latest_merge_request_diff_id" t.integer "latest_merge_request_diff_id"
t.string "rebase_commit_sha" t.string "rebase_commit_sha"
t.boolean "allow_maintainer_to_push"
end end
add_index "merge_requests", ["assignee_id"], name: "index_merge_requests_on_assignee_id", using: :btree add_index "merge_requests", ["assignee_id"], name: "index_merge_requests_on_assignee_id", using: :btree
......
...@@ -529,18 +529,19 @@ Creates a new merge request. ...@@ -529,18 +529,19 @@ Creates a new merge request.
POST /projects/:id/merge_requests POST /projects/:id/merge_requests
``` ```
| Attribute | Type | Required | Description | | Attribute | Type | Required | Description |
| --------- | ---- | -------- | ----------- | | --------- | ---- | -------- | ----------- |
| `id` | integer/string | yes | The ID or [URL-encoded path of the project](README.md#namespaced-path-encoding) owned by the authenticated user | | `id` | integer/string | yes | The ID or [URL-encoded path of the project](README.md#namespaced-path-encoding) owned by the authenticated user |
| `source_branch` | string | yes | The source branch | | `source_branch` | string | yes | The source branch |
| `target_branch` | string | yes | The target branch | | `target_branch` | string | yes | The target branch |
| `title` | string | yes | Title of MR | | `title` | string | yes | Title of MR |
| `assignee_id` | integer | no | Assignee user ID | | `assignee_id` | integer | no | Assignee user ID |
| `description` | string | no | Description of MR | | `description` | string | no | Description of MR |
| `target_project_id` | integer | no | The target project (numeric id) | | `target_project_id` | integer | no | The target project (numeric id) |
| `labels` | string | no | Labels for MR as a comma-separated list | | `labels` | string | no | Labels for MR as a comma-separated list |
| `milestone_id` | integer | no | The ID of a milestone | | `milestone_id` | integer | no | The ID of a milestone |
| `remove_source_branch` | boolean | no | Flag indicating if a merge request should remove the source branch when merging | | `remove_source_branch` | boolean | no | Flag indicating if a merge request should remove the source branch when merging |
| `allow_maintainer_to_push` | boolean | no | Whether or not a maintainer of the target project can push to the source branch |
```json ```json
{ {
...@@ -548,7 +549,7 @@ POST /projects/:id/merge_requests ...@@ -548,7 +549,7 @@ POST /projects/:id/merge_requests
"iid": 1, "iid": 1,
"target_branch": "master", "target_branch": "master",
"source_branch": "test1", "source_branch": "test1",
"project_id": 3, "project_id": 4,
"title": "test1", "title": "test1",
"state": "opened", "state": "opened",
"upvotes": 0, "upvotes": 0,
...@@ -569,7 +570,7 @@ POST /projects/:id/merge_requests ...@@ -569,7 +570,7 @@ POST /projects/:id/merge_requests
"state": "active", "state": "active",
"created_at": "2012-04-29T08:46:00Z" "created_at": "2012-04-29T08:46:00Z"
}, },
"source_project_id": 4, "source_project_id": 3,
"target_project_id": 4, "target_project_id": 4,
"labels": [ ], "labels": [ ],
"description": "fixed login page css paddings", "description": "fixed login page css paddings",
...@@ -596,6 +597,7 @@ POST /projects/:id/merge_requests ...@@ -596,6 +597,7 @@ POST /projects/:id/merge_requests
"force_remove_source_branch": false, "force_remove_source_branch": false,
"web_url": "http://example.com/example/example/merge_requests/1", "web_url": "http://example.com/example/example/merge_requests/1",
"discussion_locked": false, "discussion_locked": false,
"allow_maintainer_to_push": false,
"time_stats": { "time_stats": {
"time_estimate": 0, "time_estimate": 0,
"total_time_spent": 0, "total_time_spent": 0,
...@@ -613,19 +615,20 @@ Updates an existing merge request. You can change the target branch, title, or e ...@@ -613,19 +615,20 @@ Updates an existing merge request. You can change the target branch, title, or e
PUT /projects/:id/merge_requests/:merge_request_iid PUT /projects/:id/merge_requests/:merge_request_iid
``` ```
| Attribute | Type | Required | Description | | Attribute | Type | Required | Description |
| --------- | ---- | -------- | ----------- | | --------- | ---- | -------- | ----------- |
| `id` | integer/string | yes | The ID or [URL-encoded path of the project](README.md#namespaced-path-encoding) owned by the authenticated user | | `id` | integer/string | yes | The ID or [URL-encoded path of the project](README.md#namespaced-path-encoding) owned by the authenticated user |
| `merge_request_iid` | integer | yes | The ID of a merge request | | `merge_request_iid` | integer | yes | The ID of a merge request |
| `target_branch` | string | no | The target branch | | `target_branch` | string | no | The target branch |
| `title` | string | no | Title of MR | | `title` | string | no | Title of MR |
| `assignee_id` | integer | no | The ID of the user to assign the merge request to. Set to `0` or provide an empty value to unassign all assignees. | | `assignee_id` | integer | no | The ID of the user to assign the merge request to. Set to `0` or provide an empty value to unassign all assignees. |
| `milestone_id` | integer | no | The ID of a milestone to assign the merge request to. Set to `0` or provide an empty value to unassign a milestone.| | `milestone_id` | integer | no | The ID of a milestone to assign the merge request to. Set to `0` or provide an empty value to unassign a milestone.|
| `labels` | string | no | Comma-separated label names for a merge request. Set to an empty string to unassign all labels. | | `labels` | string | no | Comma-separated label names for a merge request. Set to an empty string to unassign all labels. |
| `description` | string | no | Description of MR | | `description` | string | no | Description of MR |
| `state_event` | string | no | New state (close/reopen) | | `state_event` | string | no | New state (close/reopen) |
| `remove_source_branch` | boolean | no | Flag indicating if a merge request should remove the source branch when merging | | `remove_source_branch` | boolean | no | Flag indicating if a merge request should remove the source branch when merging |
| `discussion_locked` | boolean | no | Flag indicating if the merge request's discussion is locked. If the discussion is locked only project members can add, edit or resolve comments. | | `discussion_locked` | boolean | no | Flag indicating if the merge request's discussion is locked. If the discussion is locked only project members can add, edit or resolve comments. |
| `allow_maintainer_to_push` | boolean | no | Whether or not a maintainer of the target project can push to the source branch |
Must include at least one non-required attribute from above. Must include at least one non-required attribute from above.
...@@ -634,7 +637,7 @@ Must include at least one non-required attribute from above. ...@@ -634,7 +637,7 @@ Must include at least one non-required attribute from above.
"id": 1, "id": 1,
"iid": 1, "iid": 1,
"target_branch": "master", "target_branch": "master",
"project_id": 3, "project_id": 4,
"title": "test1", "title": "test1",
"state": "opened", "state": "opened",
"upvotes": 0, "upvotes": 0,
...@@ -655,7 +658,7 @@ Must include at least one non-required attribute from above. ...@@ -655,7 +658,7 @@ Must include at least one non-required attribute from above.
"state": "active", "state": "active",
"created_at": "2012-04-29T08:46:00Z" "created_at": "2012-04-29T08:46:00Z"
}, },
"source_project_id": 4, "source_project_id": 3,
"target_project_id": 4, "target_project_id": 4,
"labels": [ ], "labels": [ ],
"description": "description1", "description": "description1",
...@@ -682,6 +685,7 @@ Must include at least one non-required attribute from above. ...@@ -682,6 +685,7 @@ Must include at least one non-required attribute from above.
"force_remove_source_branch": false, "force_remove_source_branch": false,
"web_url": "http://example.com/example/example/merge_requests/1", "web_url": "http://example.com/example/example/merge_requests/1",
"discussion_locked": false, "discussion_locked": false,
"allow_maintainer_to_push": false,
"time_stats": { "time_stats": {
"time_estimate": 0, "time_estimate": 0,
"total_time_spent": 0, "total_time_spent": 0,
......
...@@ -28,6 +28,7 @@ With GitLab merge requests, you can: ...@@ -28,6 +28,7 @@ With GitLab merge requests, you can:
- Enable [fast-forward merge requests](#fast-forward-merge-requests) - Enable [fast-forward merge requests](#fast-forward-merge-requests)
- Enable [semi-linear history merge requests](#semi-linear-history-merge-requests) as another security layer to guarantee the pipeline is passing in the target branch - Enable [semi-linear history merge requests](#semi-linear-history-merge-requests) as another security layer to guarantee the pipeline is passing in the target branch
- [Create new merge requests by email](#create-new-merge-requests-by-email) - [Create new merge requests by email](#create-new-merge-requests-by-email)
- Allow maintainers of the target project to push directly to the fork by [allowing edits from maintainers](maintainer_access.md)
With **[GitLab Enterprise Edition][ee]**, you can also: With **[GitLab Enterprise Edition][ee]**, you can also:
......
# Allow maintainer pushes for merge requests accross forks
This feature is available for merge requests across forked projects that are
publicly accessible. It makes it easier for maintainers of projects to collaborate
on merge requests across forks.
When enabling this feature for a merge request, you give can give members with push access to the target project rights to edit files on the source branch of the merge request.
The feature can only be enabled by users who already have push access to the source project. And only lasts while the merge request is open.
Enable this functionality while creating a merge request:
![Enable maintainer edits](./img/allow_maintainer_push.png)
...@@ -547,6 +547,7 @@ module API ...@@ -547,6 +547,7 @@ module API
expose :discussion_locked expose :discussion_locked
expose :should_remove_source_branch?, as: :should_remove_source_branch expose :should_remove_source_branch?, as: :should_remove_source_branch
expose :force_remove_source_branch?, as: :force_remove_source_branch expose :force_remove_source_branch?, as: :force_remove_source_branch
expose :allow_maintainer_to_push, if: -> (merge_request, _) { merge_request.for_fork? }
expose :web_url do |merge_request, options| expose :web_url do |merge_request, options|
Gitlab::UrlBuilder.build(merge_request) Gitlab::UrlBuilder.build(merge_request)
......
...@@ -144,6 +144,7 @@ module API ...@@ -144,6 +144,7 @@ module API
optional :milestone_id, type: Integer, desc: 'The ID of a milestone to assign the merge request' optional :milestone_id, type: Integer, desc: 'The ID of a milestone to assign the merge request'
optional :labels, type: String, desc: 'Comma-separated list of label names' optional :labels, type: String, desc: 'Comma-separated list of label names'
optional :remove_source_branch, type: Boolean, desc: 'Remove source branch when merging' optional :remove_source_branch, type: Boolean, desc: 'Remove source branch when merging'
optional :allow_maintainer_to_push, type: Boolean, desc: 'Whether a maintainer of the target project can push to the source project'
use :optional_params_ee use :optional_params_ee
end end
......
...@@ -47,7 +47,7 @@ module Gitlab ...@@ -47,7 +47,7 @@ module Gitlab
protected protected
def push_checks def push_checks
if user_access.cannot_do_action?(:push_code) unless can_push?
raise GitAccess::UnauthorizedError, ERROR_MESSAGES[:push_code] raise GitAccess::UnauthorizedError, ERROR_MESSAGES[:push_code]
end end
end end
...@@ -183,6 +183,11 @@ module Gitlab ...@@ -183,6 +183,11 @@ module Gitlab
def commits def commits
@commits ||= project.repository.new_commits(newrev) @commits ||= project.repository.new_commits(newrev)
end end
def can_push?
user_access.can_do_action?(:push_code) ||
user_access.can_push_to_branch?(branch_name)
end
end end
end end
end end
...@@ -68,8 +68,10 @@ module Gitlab ...@@ -68,8 +68,10 @@ module Gitlab
return true if project.user_can_push_to_empty_repo?(user) return true if project.user_can_push_to_empty_repo?(user)
protected_branch_accessible_to?(ref, action: :push) protected_branch_accessible_to?(ref, action: :push)
elsif user.can?(:push_code, project)
true
else else
user.can?(:push_code, project) project.branch_allows_maintainer_push?(user, ref)
end end
end end
......
...@@ -8,8 +8,8 @@ msgid "" ...@@ -8,8 +8,8 @@ msgid ""
msgstr "" msgstr ""
"Project-Id-Version: gitlab 1.0.0\n" "Project-Id-Version: gitlab 1.0.0\n"
"Report-Msgid-Bugs-To: \n" "Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2018-03-05 13:02-0600\n" "POT-Creation-Date: 2018-03-06 17:36+0100\n"
"PO-Revision-Date: 2018-03-05 13:02-0600\n" "PO-Revision-Date: 2018-03-06 17:36+0100\n"
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n" "Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
"Language-Team: LANGUAGE <LL@li.org>\n" "Language-Team: LANGUAGE <LL@li.org>\n"
"Language: \n" "Language: \n"
...@@ -102,6 +102,9 @@ msgstr "" ...@@ -102,6 +102,9 @@ msgstr ""
msgid "A collection of graphs regarding Continuous Integration" msgid "A collection of graphs regarding Continuous Integration"
msgstr "" msgstr ""
msgid "A new branch will be created in your fork and a new merge request will be started."
msgstr ""
msgid "A project is where you house your files (repository), plan your work (issues), and publish your documentation (wiki), %{among_other_things_link}." msgid "A project is where you house your files (repository), plan your work (issues), and publish your documentation (wiki), %{among_other_things_link}."
msgstr "" msgstr ""
...@@ -207,6 +210,9 @@ msgstr "" ...@@ -207,6 +210,9 @@ msgstr ""
msgid "All features are enabled for blank projects, from templates, or when importing, but you can disable them afterward in the project settings." msgid "All features are enabled for blank projects, from templates, or when importing, but you can disable them afterward in the project settings."
msgstr "" msgstr ""
msgid "Allow edits from maintainers"
msgstr ""
msgid "Allows you to add and manage Kubernetes clusters." msgid "Allows you to add and manage Kubernetes clusters."
msgstr "" msgstr ""
...@@ -288,9 +294,6 @@ msgstr "" ...@@ -288,9 +294,6 @@ msgstr ""
msgid "Are you sure you want to delete this pipeline schedule?" msgid "Are you sure you want to delete this pipeline schedule?"
msgstr "" msgstr ""
msgid "Are you sure you want to discard your changes?"
msgstr ""
msgid "Are you sure you want to reset registration token?" msgid "Are you sure you want to reset registration token?"
msgstr "" msgstr ""
...@@ -392,9 +395,6 @@ msgstr[1] "" ...@@ -392,9 +395,6 @@ msgstr[1] ""
msgid "Branch <strong>%{branch_name}</strong> was created. To set up auto deploy, choose a GitLab CI Yaml template and commit your changes. %{link_to_autodeploy_doc}" msgid "Branch <strong>%{branch_name}</strong> was created. To set up auto deploy, choose a GitLab CI Yaml template and commit your changes. %{link_to_autodeploy_doc}"
msgstr "" msgstr ""
msgid "Branch has changed"
msgstr ""
msgid "Branch is already taken" msgid "Branch is already taken"
msgstr "" msgstr ""
...@@ -410,6 +410,15 @@ msgstr "" ...@@ -410,6 +410,15 @@ msgstr ""
msgid "Branches" msgid "Branches"
msgstr "" msgstr ""
msgid "Branches|Active"
msgstr ""
msgid "Branches|Active branches"
msgstr ""
msgid "Branches|All"
msgstr ""
msgid "Branches|Cant find HEAD commit for this branch" msgid "Branches|Cant find HEAD commit for this branch"
msgstr "" msgstr ""
...@@ -455,12 +464,39 @@ msgstr "" ...@@ -455,12 +464,39 @@ msgstr ""
msgid "Branches|Only a project master or owner can delete a protected branch" msgid "Branches|Only a project master or owner can delete a protected branch"
msgstr "" msgstr ""
msgid "Branches|Protected branches can be managed in %{project_settings_link}" msgid "Branches|Overview"
msgstr ""
msgid "Branches|Protected branches can be managed in %{project_settings_link}."
msgstr ""
msgid "Branches|Show active branches"
msgstr ""
msgid "Branches|Show all branches"
msgstr ""
msgid "Branches|Show more active branches"
msgstr ""
msgid "Branches|Show more stale branches"
msgstr ""
msgid "Branches|Show overview of the branches"
msgstr ""
msgid "Branches|Show stale branches"
msgstr "" msgstr ""
msgid "Branches|Sort by" msgid "Branches|Sort by"
msgstr "" msgstr ""
msgid "Branches|Stale"
msgstr ""
msgid "Branches|Stale branches"
msgstr ""
msgid "Branches|The default branch cannot be deleted" msgid "Branches|The default branch cannot be deleted"
msgstr "" msgstr ""
...@@ -512,9 +548,6 @@ msgstr "" ...@@ -512,9 +548,6 @@ msgstr ""
msgid "Cancel" msgid "Cancel"
msgstr "" msgstr ""
msgid "Cancel edit"
msgstr ""
msgid "Cannot modify managed Kubernetes cluster" msgid "Cannot modify managed Kubernetes cluster"
msgstr "" msgstr ""
...@@ -662,6 +695,9 @@ msgstr "" ...@@ -662,6 +695,9 @@ msgstr ""
msgid "Clone repository" msgid "Clone repository"
msgstr "" msgstr ""
msgid "Close"
msgstr ""
msgid "ClusterIntegration|%{appList} was successfully installed on your Kubernetes cluster" msgid "ClusterIntegration|%{appList} was successfully installed on your Kubernetes cluster"
msgstr "" msgstr ""
...@@ -1077,6 +1113,9 @@ msgstr "" ...@@ -1077,6 +1113,9 @@ msgstr ""
msgid "ContainerRegistry|With the Docker Container Registry integrated into GitLab, every project can have its own space to store its Docker images." msgid "ContainerRegistry|With the Docker Container Registry integrated into GitLab, every project can have its own space to store its Docker images."
msgstr "" msgstr ""
msgid "Contribution"
msgstr ""
msgid "Contribution guide" msgid "Contribution guide"
msgstr "" msgstr ""
...@@ -1128,9 +1167,6 @@ msgstr "" ...@@ -1128,9 +1167,6 @@ msgstr ""
msgid "Create empty bare repository" msgid "Create empty bare repository"
msgstr "" msgstr ""
msgid "Create file"
msgstr ""
msgid "Create lists from labels. Issues with that label appear in that list." msgid "Create lists from labels. Issues with that label appear in that list."
msgstr "" msgstr ""
...@@ -1140,15 +1176,6 @@ msgstr "" ...@@ -1140,15 +1176,6 @@ msgstr ""
msgid "Create merge request and branch" msgid "Create merge request and branch"
msgstr "" msgstr ""
msgid "Create new branch"
msgstr ""
msgid "Create new directory"
msgstr ""
msgid "Create new file"
msgstr ""
msgid "Create new label" msgid "Create new label"
msgstr "" msgstr ""
...@@ -1238,9 +1265,6 @@ msgstr "" ...@@ -1238,9 +1265,6 @@ msgstr ""
msgid "Directory name" msgid "Directory name"
msgstr "" msgstr ""
msgid "Discard changes"
msgstr ""
msgid "Dismiss Cycle Analytics introduction box" msgid "Dismiss Cycle Analytics introduction box"
msgstr "" msgstr ""
...@@ -1421,9 +1445,6 @@ msgstr "" ...@@ -1421,9 +1445,6 @@ msgstr ""
msgid "Fields on this page are now uneditable, you can configure" msgid "Fields on this page are now uneditable, you can configure"
msgstr "" msgstr ""
msgid "File name"
msgstr ""
msgid "Files" msgid "Files"
msgstr "" msgstr ""
...@@ -1439,6 +1460,9 @@ msgstr "" ...@@ -1439,6 +1460,9 @@ msgstr ""
msgid "Find file" msgid "Find file"
msgstr "" msgstr ""
msgid "Finished"
msgstr ""
msgid "FirstPushedBy|First" msgid "FirstPushedBy|First"
msgstr "" msgstr ""
...@@ -1495,6 +1519,9 @@ msgstr "" ...@@ -1495,6 +1519,9 @@ msgstr ""
msgid "Gitaly Servers" msgid "Gitaly Servers"
msgstr "" msgstr ""
msgid "Go back"
msgstr ""
msgid "Go to your fork" msgid "Go to your fork"
msgstr "" msgstr ""
...@@ -1701,6 +1728,15 @@ msgstr "" ...@@ -1701,6 +1728,15 @@ msgstr ""
msgid "LFSStatus|Enabled" msgid "LFSStatus|Enabled"
msgstr "" msgstr ""
msgid "Label"
msgstr ""
msgid "LabelSelect|%{firstLabelName} +%{remainingLabelCount} more"
msgstr ""
msgid "LabelSelect|%{labelsString}, and %{remainingLabelCount} more"
msgstr ""
msgid "Labels" msgid "Labels"
msgstr "" msgstr ""
...@@ -1760,9 +1796,6 @@ msgstr "" ...@@ -1760,9 +1796,6 @@ msgstr ""
msgid "Leave project" msgid "Leave project"
msgstr "" msgstr ""
msgid "Loading the GitLab IDE..."
msgstr ""
msgid "Lock" msgid "Lock"
msgstr "" msgstr ""
...@@ -1945,6 +1978,12 @@ msgstr "" ...@@ -1945,6 +1978,12 @@ msgstr ""
msgid "Not available" msgid "Not available"
msgstr "" msgstr ""
msgid "Not available for private projects"
msgstr ""
msgid "Not available for protected branches"
msgstr ""
msgid "Not confidential" msgid "Not confidential"
msgstr "" msgstr ""
...@@ -2071,6 +2110,9 @@ msgstr "" ...@@ -2071,6 +2110,9 @@ msgstr ""
msgid "Password" msgid "Password"
msgstr "" msgstr ""
msgid "Pending"
msgstr ""
msgid "Pipeline" msgid "Pipeline"
msgstr "" msgstr ""
...@@ -2149,9 +2191,30 @@ msgstr "" ...@@ -2149,9 +2191,30 @@ msgstr ""
msgid "Pipelines|Build with confidence" msgid "Pipelines|Build with confidence"
msgstr "" msgstr ""
msgid "Pipelines|CI Lint"
msgstr ""
msgid "Pipelines|Clear Runner Caches"
msgstr ""
msgid "Pipelines|Get started with Pipelines" msgid "Pipelines|Get started with Pipelines"
msgstr "" msgstr ""
msgid "Pipelines|Loading Pipelines"
msgstr ""
msgid "Pipelines|Run Pipeline"
msgstr ""
msgid "Pipelines|There are currently no %{scope} pipelines."
msgstr ""
msgid "Pipelines|There are currently no pipelines."
msgstr ""
msgid "Pipelines|This project is not currently set up to run pipelines."
msgstr ""
msgid "Pipeline|Retry pipeline" msgid "Pipeline|Retry pipeline"
msgstr "" msgstr ""
...@@ -2404,6 +2467,9 @@ msgstr "" ...@@ -2404,6 +2467,9 @@ msgstr ""
msgid "Public - The project can be accessed without any authentication." msgid "Public - The project can be accessed without any authentication."
msgstr "" msgstr ""
msgid "Push access to this project is necessary in order to enable this option"
msgstr ""
msgid "Push events" msgid "Push events"
msgstr "" msgstr ""
...@@ -2490,6 +2556,9 @@ msgstr "" ...@@ -2490,6 +2556,9 @@ msgstr ""
msgid "Revert this merge request" msgid "Revert this merge request"
msgstr "" msgstr ""
msgid "Running"
msgstr ""
msgid "SSH Keys" msgid "SSH Keys"
msgstr "" msgstr ""
...@@ -2511,6 +2580,9 @@ msgstr "" ...@@ -2511,6 +2580,9 @@ msgstr ""
msgid "Scheduling Pipelines" msgid "Scheduling Pipelines"
msgstr "" msgstr ""
msgid "Search"
msgstr ""
msgid "Search branches and tags" msgid "Search branches and tags"
msgstr "" msgstr ""
...@@ -3278,9 +3350,6 @@ msgstr "" ...@@ -3278,9 +3350,6 @@ msgstr ""
msgid "We want to be sure it is you, please confirm you are not a robot." msgid "We want to be sure it is you, please confirm you are not a robot."
msgstr "" msgstr ""
msgid "Web IDE"
msgstr ""
msgid "Wiki" msgid "Wiki"
msgstr "" msgstr ""
...@@ -3395,13 +3464,13 @@ msgstr "" ...@@ -3395,13 +3464,13 @@ msgstr ""
msgid "You are going to remove %{group_name}. Removed groups CANNOT be restored! Are you ABSOLUTELY sure?" msgid "You are going to remove %{group_name}. Removed groups CANNOT be restored! Are you ABSOLUTELY sure?"
msgstr "" msgstr ""
msgid "You are going to remove %{project_name_with_namespace}. Removed project CANNOT be restored! Are you ABSOLUTELY sure?" msgid "You are going to remove %{project_full_name}. Removed project CANNOT be restored! Are you ABSOLUTELY sure?"
msgstr "" msgstr ""
msgid "You are going to remove the fork relationship to source project %{forked_from_project}. Are you ABSOLUTELY sure?" msgid "You are going to remove the fork relationship to source project %{forked_from_project}. Are you ABSOLUTELY sure?"
msgstr "" msgstr ""
msgid "You are going to transfer %{project_name_with_namespace} to another owner. Are you ABSOLUTELY sure?" msgid "You are going to transfer %{project_full_name} to another owner. Are you ABSOLUTELY sure?"
msgstr "" msgstr ""
msgid "You are on a read-only GitLab instance." msgid "You are on a read-only GitLab instance."
...@@ -3467,6 +3536,9 @@ msgstr "" ...@@ -3467,6 +3536,9 @@ msgstr ""
msgid "Your Kubernetes cluster information on this page is still editable, but you are advised to disable and reconfigure" msgid "Your Kubernetes cluster information on this page is still editable, but you are advised to disable and reconfigure"
msgstr "" msgstr ""
msgid "Your changes can be committed to %{branch_name} because a merge request is open."
msgstr ""
msgid "Your comment will not be visible to the public." msgid "Your comment will not be visible to the public."
msgstr "" msgstr ""
...@@ -3513,6 +3585,9 @@ msgstr[1] "" ...@@ -3513,6 +3585,9 @@ msgstr[1] ""
msgid "mrWidget| Please restore it or use a different %{missingBranchName} branch" msgid "mrWidget| Please restore it or use a different %{missingBranchName} branch"
msgstr "" msgstr ""
msgid "mrWidget|Allows edits from maintainers"
msgstr ""
msgid "mrWidget|Cancel automatic merge" msgid "mrWidget|Cancel automatic merge"
msgstr "" msgstr ""
......
require 'spec_helper'
describe 'a maintainer edits files on a source-branch of an MR from a fork', :js do
include ProjectForksHelper
let(:user) { create(:user, username: 'the-maintainer') }
let(:target_project) { create(:project, :public, :repository) }
let(:author) { create(:user, username: 'mr-authoring-machine') }
let(:source_project) { fork_project(target_project, author, repository: true) }
let(:merge_request) do
create(:merge_request,
source_project: source_project,
target_project: target_project,
source_branch: 'fix',
target_branch: 'master',
author: author,
allow_maintainer_to_push: true)
end
before do
target_project.add_master(user)
sign_in(user)
visit project_merge_request_path(target_project, merge_request)
click_link 'Changes'
wait_for_requests
first('.js-file-title').click_link 'Edit'
wait_for_requests
end
it 'mentions commits will go to the source branch' do
expect(page).to have_content('Your changes can be committed to fix because a merge request is open.')
end
it 'allows committing to the source branch' do
find('.ace_text-input', visible: false).send_keys('Updated the readme')
click_button 'Commit changes'
wait_for_requests
expect(page).to have_content('Your changes have been successfully committed')
expect(page).to have_content('Updated the readme')
end
end
require 'spec_helper'
describe 'create a merge request that allows maintainers to push', :js do
include ProjectForksHelper
let(:user) { create(:user) }
let(:target_project) { create(:project, :public, :repository) }
let(:source_project) { fork_project(target_project, user, repository: true, namespace: user.namespace) }
def visit_new_merge_request
visit project_new_merge_request_path(
source_project,
merge_request: {
source_project_id: source_project.id,
target_project_id: target_project.id,
source_branch: 'fix',
target_branch: 'master'
})
end
before do
sign_in(user)
end
it 'allows setting maintainer push possible' do
visit_new_merge_request
check 'Allow edits from maintainers'
click_button 'Submit merge request'
wait_for_requests
expect(page).to have_content('Allows edits from maintainers')
end
it 'shows a message when one of the projects is private' do
source_project.update!(visibility_level: Gitlab::VisibilityLevel::PRIVATE)
visit_new_merge_request
expect(page).to have_content('Not available for private projects')
end
it 'shows a message when the source branch is protected' do
create(:protected_branch, project: source_project, name: 'fix')
visit_new_merge_request
expect(page).to have_content('Not available for protected branches')
end
context 'when the merge request is being created within the same project' do
let(:source_project) { target_project }
it 'hides the checkbox if the merge request is being created within the same project' do
target_project.add_developer(user)
visit_new_merge_request
expect(page).not_to have_content('Allows edits from maintainers')
end
end
context 'when a maintainer tries to edit the option' do
let(:maintainer) { create(:user) }
let(:merge_request) do
create(:merge_request,
source_project: source_project,
target_project: target_project,
source_branch: 'fixes')
end
before do
target_project.add_master(maintainer)
sign_in(maintainer)
end
it 'it hides the option from maintainers' do
visit edit_project_merge_request_path(target_project, merge_request)
expect(page).not_to have_content('Allows edits from maintainers')
end
end
end
...@@ -133,13 +133,20 @@ describe 'User creates files' do ...@@ -133,13 +133,20 @@ describe 'User creates files' do
before do before do
project2.add_reporter(user) project2.add_reporter(user)
visit(project2_tree_path_root_ref) visit(project2_tree_path_root_ref)
end
it 'creates and commit new file in forked project', :js do
find('.add-to-tree').click find('.add-to-tree').click
click_link('New file') click_link('New file')
end
it 'shows a message saying the file will be committed in a fork' do
message = "A new branch will be created in your fork and a new merge request will be started."
expect(page).to have_content(message)
end
it 'creates and commit new file in forked project', :js do
expect(page).to have_selector('.file-editor') expect(page).to have_selector('.file-editor')
expect(page).to have_content
find('#editor') find('#editor')
execute_script("ace.edit('editor').setValue('*.rbca')") execute_script("ace.edit('editor').setValue('*.rbca')")
......
...@@ -12,7 +12,8 @@ ...@@ -12,7 +12,8 @@
"rebase_in_progress": { "type": "boolean" }, "rebase_in_progress": { "type": "boolean" },
"assignee_id": { "type": ["integer", "null"] }, "assignee_id": { "type": ["integer", "null"] },
"subscribed": { "type": ["boolean", "null"] }, "subscribed": { "type": ["boolean", "null"] },
"participants": { "type": "array" } "participants": { "type": "array" },
"allow_maintainer_to_push": { "type": "boolean"}
}, },
"additionalProperties": false "additionalProperties": false
} }
...@@ -30,6 +30,7 @@ ...@@ -30,6 +30,7 @@
"source_project_id": { "type": "integer" }, "source_project_id": { "type": "integer" },
"target_branch": { "type": "string" }, "target_branch": { "type": "string" },
"target_project_id": { "type": "integer" }, "target_project_id": { "type": "integer" },
"allow_maintainer_to_push": { "type": "boolean"},
"metrics": { "metrics": {
"oneOf": [ "oneOf": [
{ "type": "null" }, { "type": "null" },
......
...@@ -80,7 +80,8 @@ ...@@ -80,7 +80,8 @@
"total_time_spent": { "type": "integer" }, "total_time_spent": { "type": "integer" },
"human_time_estimate": { "type": ["string", "null"] }, "human_time_estimate": { "type": ["string", "null"] },
"human_total_time_spent": { "type": ["string", "null"] } "human_total_time_spent": { "type": ["string", "null"] }
} },
"allow_maintainer_to_push": { "type": ["boolean", "null"] }
}, },
"required": [ "required": [
"id", "iid", "project_id", "title", "description", "id", "iid", "project_id", "title", "description",
......
...@@ -62,4 +62,13 @@ describe TreeHelper do ...@@ -62,4 +62,13 @@ describe TreeHelper do
end end
end end
end end
describe '#commit_in_single_accessible_branch' do
it 'escapes HTML from the branch name' do
helper.instance_variable_set(:@branch_name, "<script>alert('escape me!');</script>")
escaped_branch_name = '&lt;script&gt;alert(&#39;escape me!&#39;);&lt;/script&gt;'
expect(helper.commit_in_single_accessible_branch).to include(escaped_branch_name)
end
end
end end
import Vue from 'vue';
import maintainerEditComponent from '~/vue_merge_request_widget/components/mr_widget_maintainer_edit.vue';
import mountComponent from 'spec/helpers/vue_mount_component_helper';
describe('MRWidgetAuthor', () => {
let vm;
beforeEach(() => {
const Component = Vue.extend(maintainerEditComponent);
vm = mountComponent(Component, {
maintainerEditAllowed: true,
});
});
afterEach(() => {
vm.$destroy();
});
it('renders the message when maintainers are allowed to edit', () => {
expect(vm.$el.textContent.trim()).toEqual('Allows edits from maintainers');
});
});
...@@ -349,6 +349,7 @@ describe('mrWidgetOptions', () => { ...@@ -349,6 +349,7 @@ describe('mrWidgetOptions', () => {
expect(comps['mr-widget-pipeline-blocked']).toBeDefined(); expect(comps['mr-widget-pipeline-blocked']).toBeDefined();
expect(comps['mr-widget-pipeline-failed']).toBeDefined(); expect(comps['mr-widget-pipeline-failed']).toBeDefined();
expect(comps['mr-widget-merge-when-pipeline-succeeds']).toBeDefined(); expect(comps['mr-widget-merge-when-pipeline-succeeds']).toBeDefined();
expect(comps['mr-widget-maintainer-edit']).toBeDefined();
}); });
}); });
......
...@@ -30,9 +30,10 @@ describe Gitlab::Checks::ChangeAccess do ...@@ -30,9 +30,10 @@ describe Gitlab::Checks::ChangeAccess do
end end
end end
context 'when the user is not allowed to push code' do context 'when the user is not allowed to push to the repo' do
it 'raises an error' do it 'raises an error' do
expect(user_access).to receive(:can_do_action?).with(:push_code).and_return(false) expect(user_access).to receive(:can_do_action?).with(:push_code).and_return(false)
expect(user_access).to receive(:can_push_to_branch?).with('master').and_return(false)
expect { subject.exec }.to raise_error(Gitlab::GitAccess::UnauthorizedError, 'You are not allowed to push code to this project.') expect { subject.exec }.to raise_error(Gitlab::GitAccess::UnauthorizedError, 'You are not allowed to push code to this project.')
end end
......
...@@ -278,6 +278,7 @@ project: ...@@ -278,6 +278,7 @@ project:
- custom_attributes - custom_attributes
- lfs_file_locks - lfs_file_locks
- project_badges - project_badges
- source_of_merge_requests
award_emoji: award_emoji:
- awardable - awardable
- user - user
......
...@@ -168,6 +168,7 @@ MergeRequest: ...@@ -168,6 +168,7 @@ MergeRequest:
- last_edited_by_id - last_edited_by_id
- head_pipeline_id - head_pipeline_id
- discussion_locked - discussion_locked
- allow_maintainer_to_push
MergeRequestDiff: MergeRequestDiff:
- id - id
- state - state
......
require 'spec_helper' require 'spec_helper'
describe Gitlab::UserAccess do describe Gitlab::UserAccess do
include ProjectForksHelper
let(:access) { described_class.new(user, project: project) } let(:access) { described_class.new(user, project: project) }
let(:project) { create(:project, :repository) } let(:project) { create(:project, :repository) }
let(:user) { create(:user) } let(:user) { create(:user) }
...@@ -118,6 +120,39 @@ describe Gitlab::UserAccess do ...@@ -118,6 +120,39 @@ describe Gitlab::UserAccess do
end end
end end
describe 'allowing pushes to maintainers of forked projects' do
let(:canonical_project) { create(:project, :public, :repository) }
let(:project) { fork_project(canonical_project, create(:user), repository: true) }
before do
create(
:merge_request,
target_project: canonical_project,
source_project: project,
source_branch: 'awesome-feature',
allow_maintainer_to_push: true
)
end
it 'allows users that have push access to the canonical project to push to the MR branch' do
canonical_project.add_developer(user)
expect(access.can_push_to_branch?('awesome-feature')).to be_truthy
end
it 'does not allow the user to push to other branches' do
canonical_project.add_developer(user)
expect(access.can_push_to_branch?('master')).to be_falsey
end
it 'does not allow the user to push if he does not have push access to the canonical project' do
canonical_project.add_guest(user)
expect(access.can_push_to_branch?('awesome-feature')).to be_falsey
end
end
describe 'merge to protected branch if allowed for developers' do describe 'merge to protected branch if allowed for developers' do
before do before do
@branch = create :protected_branch, :developers_can_merge, project: project @branch = create :protected_branch, :developers_can_merge, project: project
......
...@@ -2084,4 +2084,82 @@ describe MergeRequest do ...@@ -2084,4 +2084,82 @@ describe MergeRequest do
it_behaves_like 'checking whether a rebase is in progress' it_behaves_like 'checking whether a rebase is in progress'
end end
end end
describe '#allow_maintainer_to_push' do
let(:merge_request) do
build(:merge_request, source_branch: 'fixes', allow_maintainer_to_push: true)
end
it 'is false when pushing by a maintainer is not possible' do
expect(merge_request).to receive(:maintainer_push_possible?) { false }
expect(merge_request.allow_maintainer_to_push).to be_falsy
end
it 'is true when pushing by a maintainer is possible' do
expect(merge_request).to receive(:maintainer_push_possible?) { true }
expect(merge_request.allow_maintainer_to_push).to be_truthy
end
end
describe '#maintainer_push_possible?' do
let(:merge_request) do
build(:merge_request, source_branch: 'fixes')
end
before do
allow(ProtectedBranch).to receive(:protected?) { false }
end
it 'does not allow maintainer to push if the source project is the same as the target' do
merge_request.target_project = merge_request.source_project = create(:project, :public)
expect(merge_request.maintainer_push_possible?).to be_falsy
end
it 'allows maintainer to push when both source and target are public' do
merge_request.target_project = build(:project, :public)
merge_request.source_project = build(:project, :public)
expect(merge_request.maintainer_push_possible?).to be_truthy
end
it 'is not available for protected branches' do
merge_request.target_project = build(:project, :public)
merge_request.source_project = build(:project, :public)
expect(ProtectedBranch).to receive(:protected?)
.with(merge_request.source_project, 'fixes')
.and_return(true)
expect(merge_request.maintainer_push_possible?).to be_falsy
end
end
describe '#can_allow_maintainer_to_push?' do
let(:target_project) { create(:project, :public) }
let(:source_project) { fork_project(target_project) }
let(:merge_request) do
create(:merge_request,
source_project: source_project,
source_branch: 'fixes',
target_project: target_project)
end
let(:user) { create(:user) }
before do
allow(merge_request).to receive(:maintainer_push_possible?) { true }
end
it 'is false if the user does not have push access to the source project' do
expect(merge_request.can_allow_maintainer_to_push?(user)).to be_falsy
end
it 'is true when the user has push access to the source project' do
source_project.add_developer(user)
expect(merge_request.can_allow_maintainer_to_push?(user)).to be_truthy
end
end
end end
require 'spec_helper' require 'spec_helper'
describe Project do describe Project do
include ProjectForksHelper
describe 'associations' do describe 'associations' do
it { is_expected.to belong_to(:group) } it { is_expected.to belong_to(:group) }
it { is_expected.to belong_to(:namespace) } it { is_expected.to belong_to(:namespace) }
...@@ -3378,4 +3380,103 @@ describe Project do ...@@ -3378,4 +3380,103 @@ describe Project do
end end
end end
end end
context 'with cross project merge requests' do
let(:user) { create(:user) }
let(:target_project) { create(:project, :repository) }
let(:project) { fork_project(target_project, nil, repository: true) }
let!(:merge_request) do
create(
:merge_request,
target_project: target_project,
target_branch: 'target-branch',
source_project: project,
source_branch: 'awesome-feature-1',
allow_maintainer_to_push: true
)
end
before do
target_project.add_developer(user)
end
describe '#merge_requests_allowing_push_to_user' do
it 'returns open merge requests for which the user has developer access to the target project' do
expect(project.merge_requests_allowing_push_to_user(user)).to include(merge_request)
end
it 'does not include closed merge requests' do
merge_request.close
expect(project.merge_requests_allowing_push_to_user(user)).to be_empty
end
it 'does not include merge requests for guest users' do
guest = create(:user)
target_project.add_guest(guest)
expect(project.merge_requests_allowing_push_to_user(guest)).to be_empty
end
it 'does not include the merge request for other users' do
other_user = create(:user)
expect(project.merge_requests_allowing_push_to_user(other_user)).to be_empty
end
it 'is empty when no user is passed' do
expect(project.merge_requests_allowing_push_to_user(nil)).to be_empty
end
end
describe '#branch_allows_maintainer_push?' do
it 'allows access if the user can merge the merge request' do
expect(project.branch_allows_maintainer_push?(user, 'awesome-feature-1'))
.to be_truthy
end
it 'does not allow guest users access' do
guest = create(:user)
target_project.add_guest(guest)
expect(project.branch_allows_maintainer_push?(guest, 'awesome-feature-1'))
.to be_falsy
end
it 'does not allow access to branches for which the merge request was closed' do
create(:merge_request, :closed,
target_project: target_project,
target_branch: 'target-branch',
source_project: project,
source_branch: 'rejected-feature-1',
allow_maintainer_to_push: true)
expect(project.branch_allows_maintainer_push?(user, 'rejected-feature-1'))
.to be_falsy
end
it 'does not allow access if the user cannot merge the merge request' do
create(:protected_branch, :masters_can_push, project: target_project, name: 'target-branch')
expect(project.branch_allows_maintainer_push?(user, 'awesome-feature-1'))
.to be_falsy
end
it 'caches the result' do
control = ActiveRecord::QueryRecorder.new { project.branch_allows_maintainer_push?(user, 'awesome-feature-1') }
expect { 3.times { project.branch_allows_maintainer_push?(user, 'awesome-feature-1') } }
.not_to exceed_query_limit(control)
end
context 'when the requeststore is active', :request_store do
it 'only queries per project across instances' do
control = ActiveRecord::QueryRecorder.new { project.branch_allows_maintainer_push?(user, 'awesome-feature-1') }
expect { 2.times { described_class.find(project.id).branch_allows_maintainer_push?(user, 'awesome-feature-1') } }
.not_to exceed_query_limit(control).with_threshold(2)
end
end
end
end
end end
...@@ -308,4 +308,41 @@ describe ProjectPolicy do ...@@ -308,4 +308,41 @@ describe ProjectPolicy do
it_behaves_like 'project policies as master' it_behaves_like 'project policies as master'
it_behaves_like 'project policies as owner' it_behaves_like 'project policies as owner'
it_behaves_like 'project policies as admin' it_behaves_like 'project policies as admin'
context 'when a public project has merge requests allowing access' do
include ProjectForksHelper
let(:user) { create(:user) }
let(:target_project) { create(:project, :public) }
let(:project) { fork_project(target_project) }
let!(:merge_request) do
create(
:merge_request,
target_project: target_project,
source_project: project,
allow_maintainer_to_push: true
)
end
let(:maintainer_abilities) do
%w(create_build update_build create_pipeline update_pipeline)
end
subject { described_class.new(user, project) }
it 'does not allow pushing code' do
expect_disallowed(*maintainer_abilities)
end
it 'allows pushing if the user is a member with push access to the target project' do
target_project.add_developer(user)
expect_allowed(*maintainer_abilities)
end
it 'dissallows abilities to a maintainer if the merge request was closed' do
target_project.add_developer(user)
merge_request.close!
expect_disallowed(*maintainer_abilities)
end
end
end end
...@@ -616,6 +616,25 @@ describe API::MergeRequests do ...@@ -616,6 +616,25 @@ describe API::MergeRequests do
expect(json_response['changes_count']).to eq('5+') expect(json_response['changes_count']).to eq('5+')
end end
end end
context 'for forked projects' do
let(:user2) { create(:user) }
let(:project) { create(:project, :public, :repository) }
let(:forked_project) { fork_project(project, user2, repository: true) }
let(:merge_request) do
create(:merge_request,
source_project: forked_project,
target_project: project,
source_branch: 'fixes',
allow_maintainer_to_push: true)
end
it 'includes the `allow_maintainer_to_push` field' do
get api("/projects/#{project.id}/merge_requests/#{merge_request.iid}", user)
expect(json_response['allow_maintainer_to_push']).to be_truthy
end
end
end end
describe 'GET /projects/:id/merge_requests/:merge_request_iid/participants' do describe 'GET /projects/:id/merge_requests/:merge_request_iid/participants' do
...@@ -815,6 +834,7 @@ describe API::MergeRequests do ...@@ -815,6 +834,7 @@ describe API::MergeRequests do
context 'forked projects' do context 'forked projects' do
let!(:user2) { create(:user) } let!(:user2) { create(:user) }
let(:project) { create(:project, :public, :repository) }
let!(:forked_project) { fork_project(project, user2, repository: true) } let!(:forked_project) { fork_project(project, user2, repository: true) }
let!(:unrelated_project) { create(:project, namespace: create(:user).namespace, creator_id: user2.id) } let!(:unrelated_project) { create(:project, namespace: create(:user).namespace, creator_id: user2.id) }
...@@ -872,6 +892,14 @@ describe API::MergeRequests do ...@@ -872,6 +892,14 @@ describe API::MergeRequests do
expect(response).to have_gitlab_http_status(400) expect(response).to have_gitlab_http_status(400)
end end
it 'allows setting `allow_maintainer_to_push`' do
post api("/projects/#{forked_project.id}/merge_requests", user2),
title: 'Test merge_request', source_branch: "feature_conflict", target_branch: "master",
author: user2, target_project_id: project.id, allow_maintainer_to_push: true
expect(response).to have_gitlab_http_status(201)
expect(json_response['allow_maintainer_to_push']).to be_truthy
end
context 'when target_branch and target_project_id is specified' do context 'when target_branch and target_project_id is specified' do
let(:params) do let(:params) do
{ title: 'Test merge_request', { title: 'Test merge_request',
......
require 'spec_helper' require 'spec_helper'
describe MergeRequests::UpdateService, :mailer do describe MergeRequests::UpdateService, :mailer do
include ProjectForksHelper
let(:project) { create(:project, :repository) } let(:project) { create(:project, :repository) }
let(:user) { create(:user) } let(:user) { create(:user) }
let(:user2) { create(:user) } let(:user2) { create(:user) }
...@@ -538,5 +540,40 @@ describe MergeRequests::UpdateService, :mailer do ...@@ -538,5 +540,40 @@ describe MergeRequests::UpdateService, :mailer do
let(:open_issuable) { merge_request } let(:open_issuable) { merge_request }
let(:closed_issuable) { create(:closed_merge_request, source_project: project) } let(:closed_issuable) { create(:closed_merge_request, source_project: project) }
end end
context 'setting `allow_maintainer_to_push`' do
let(:target_project) { create(:project, :public) }
let(:source_project) { fork_project(target_project) }
let(:user) { create(:user) }
let(:merge_request) do
create(:merge_request,
source_project: source_project,
source_branch: 'fixes',
target_project: target_project)
end
before do
allow(ProtectedBranch).to receive(:protected?).with(source_project, 'fixes') { false }
end
it 'does not allow a maintainer of the target project to set `allow_maintainer_to_push`' do
target_project.add_developer(user)
update_merge_request(allow_maintainer_to_push: true, title: 'Updated title')
expect(merge_request.title).to eq('Updated title')
expect(merge_request.allow_maintainer_to_push).to be_falsy
end
it 'is allowed by a user that can push to the source and can update the merge request' do
merge_request.update!(assignee: user)
source_project.add_developer(user)
update_merge_request(allow_maintainer_to_push: true, title: 'Updated title')
expect(merge_request.title).to eq('Updated title')
expect(merge_request.allow_maintainer_to_push).to be_truthy
end
end
end end
end end
...@@ -13,6 +13,7 @@ describe 'projects/tree/show' do ...@@ -13,6 +13,7 @@ describe 'projects/tree/show' do
allow(view).to receive(:can?).and_return(true) allow(view).to receive(:can?).and_return(true)
allow(view).to receive(:can_collaborate_with_project?).and_return(true) allow(view).to receive(:can_collaborate_with_project?).and_return(true)
allow(view).to receive_message_chain('user_access.can_push_to_branch?').and_return(true)
allow(view).to receive(:current_application_settings).and_return(Gitlab::CurrentSettings.current_application_settings) allow(view).to receive(:current_application_settings).and_return(Gitlab::CurrentSettings.current_application_settings)
end end
......
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