Skip to content
Snippets Groups Projects

Git push options to create a merge request, set target_branch and set merge when pipeline succeeds

Merged Luke Duncalfe requested to merge 43263-git-push-option-to-create-mr into master
9 files
+ 649
7
Compare changes
  • Side-by-side
  • Inline
Files
9
  • b7c58f4d
    To create a new merge request:
    
      git push -u origin -o merge_request.create
    
    To create a new merge request setting target branch:
    
      git push -u origin -o merge_request.create \
        -o merge_request.target=123
    
    To update an existing merge request with a new target branch:
    
      git push -u origin -o merge_request.target=123
    
    A new Gitlab::PushOptions class handles parsing and validating the push
    options array. This can be the start of the standard of GitLab accepting
    push options that follow namespacing rules. Rules are discussed in issue
    https://gitlab.com/gitlab-org/gitlab-ce/issues/43263.
    
    E.g. these push options:
    
      -o merge_request.create -o merge_request.target=123
    
    Become parsed as:
    
      {
        merge_request: {
          create: true,
          target: '123',
        }
      }
    
    And are fetched with the class via:
    
      push_options.get(:merge_request)
      push_options.get(:merge_request, :create)
      push_options.get(:merge_request, :target)
    
    A new MergeRequests::PushOptionsHandlerService takes the `merge_request`
    namespaced push options and handles creating and updating
    merge requests.
    
    Any errors encountered are passed to the existing `output` Hash in
    Api::Internal's `post_receive` endpoint, and passed to gitlab-shell
    where they're output to the user.
    
    Issue https://gitlab.com/gitlab-org/gitlab-ce/issues/43263
# frozen_string_literal: true
module MergeRequests
class PushOptionsHandlerService
attr_reader :errors
Result = Struct.new(:merge_request, :action, :success)
Error = Class.new(StandardError)
LIMIT = 10
def initialize(project, user, changes, push_options)
@project = project
@user = user
@push_options = push_options
raise Error, 'User is required' if @user.blank?
unless @push_options.values_at(:create, :target).compact.present?
raise Error, 'Push options are not valid'
end
unless @project.merge_requests_enabled?
raise Error, 'Merge requests are not enabled for project'
end
@changes_by_branch = parse_changes(changes)
@branches = @changes_by_branch.keys
if @branches.size > LIMIT
raise Error, "Too many branches pushed (#{@branches.size} were pushed, limit is #{LIMIT})"
end
@target = @push_options[:target] || @project.default_branch
@merge_requests = MergeRequest.from_project(@project)
.opened
.from_source_branches(@branches)
.to_a # fetch now
@errors = []
end
def execute
@branches.each do |branch|
execute_for_branch(branch)
end
self
end
private
# Parses changes in the push.
# Returns a hash of branch => changes_list
def parse_changes(raw_changes)
branches_and_changes = Gitlab::ChangesList.new(raw_changes).map do |change|
next unless Gitlab::Git.branch_ref?(change[:ref])
# Deleted branch
next if Gitlab::Git.blank_ref?(change[:newrev])
# Default branch
branch_name = Gitlab::Git.branch_name(change[:ref])
next if branch_name == @project.default_branch
[branch_name, change]
end.compact
Hash[branches_and_changes]
end
def execute_for_branch(branch)
merge_request = @merge_requests.find { |mr| mr.source_branch == branch }
if merge_request.blank?
unless @push_options[:create]
@errors << "merge_request.create push option is required to create a merge request for branch #{branch}"
return
end
create!(branch)
else
unless @push_options[:target]
@errors << "Merge request for branch #{branch} already exists"
return
end
update!(merge_request)
end
end
def create!(branch)
merge_request = ::MergeRequests::CreateService.new(
@project,
@user,
create_params(branch)
).execute
unless merge_request.persisted?
@errors << merge_request.errors.full_messages.to_sentence
end
end
def update!(merge_request)
return if @target == merge_request.target_branch
merge_request = ::MergeRequests::UpdateService.new(
@project,
@user,
{ target_branch: @target }
).execute(merge_request)
unless merge_request.valid?
@errors << merge_request.errors.full_messages.to_sentence
end
end
def create_params(branch)
change = @changes_by_branch[branch]
commits = @project.repository.commits_between(@project.default_branch, change[:newrev])
commits = CommitCollection.new(@project, commits)
commit = commits.without_merge_commits.first
{
assignee: @user,
source_branch: branch,
target_branch: @target,
title: commit&.title&.strip || 'New Merge Request',
description: commit&.description&.strip
}
end
end
end
Loading