Welcome to mirror list, hosted at ThFree Co, Russian Federation.

gitlab.com/gitlab-org/gitlab-foss.git - Unnamed repository; edit this file 'description' to name the repository.
summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorLuke Duncalfe <lduncalfe@eml.cc>2019-03-29 04:07:03 +0300
committerLuke Duncalfe <lduncalfe@eml.cc>2019-04-09 00:36:42 +0300
commitaa352a95df665ded5178c1b26d4492433e47714e (patch)
tree47e60787a36f6ab36d4649cef763f16b636583d8
parent225edb0d2d7737cf52ef5cd358082d08e20feaa4 (diff)
Support merge request create with push options
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
-rw-r--r--app/services/merge_requests/push_options_handler_service.rb127
-rw-r--r--changelogs/unreleased/43263-git-push-option-to-create-mr.yml5
-rw-r--r--doc/user/project/merge_requests/index.md33
-rw-r--r--lib/api/helpers/internal_helpers.rb5
-rw-r--r--lib/api/internal.rb36
-rw-r--r--lib/gitlab/push_options.rb65
-rw-r--r--spec/lib/gitlab/push_options_spec.rb91
-rw-r--r--spec/requests/api/internal_spec.rb61
-rw-r--r--spec/services/merge_requests/push_options_handler_service_spec.rb291
9 files changed, 707 insertions, 7 deletions
diff --git a/app/services/merge_requests/push_options_handler_service.rb b/app/services/merge_requests/push_options_handler_service.rb
new file mode 100644
index 00000000000..c810340c636
--- /dev/null
+++ b/app/services/merge_requests/push_options_handler_service.rb
@@ -0,0 +1,127 @@
+# frozen_string_literal: true
+
+module MergeRequests
+ class PushOptionsHandlerService
+ Error = Class.new(StandardError)
+
+ LIMIT = 10
+
+ attr_reader :branches, :changes_by_branch, :current_user, :errors,
+ :project, :push_options, :target
+
+ def initialize(project, current_user, changes, push_options)
+ @project = project
+ @current_user = current_user
+ @changes_by_branch = parse_changes(changes)
+ @push_options = push_options
+ @errors = []
+ @branches = @changes_by_branch.keys
+ @target = @push_options[:target] || @project.default_branch
+
+ raise Error, 'User is required' if @current_user.nil?
+
+ unless @project.merge_requests_enabled?
+ raise Error, 'Merge requests are not enabled for project'
+ end
+
+ if @branches.size > LIMIT
+ raise Error, "Too many branches pushed (#{@branches.size} were pushed, limit is #{LIMIT})"
+ end
+
+ if @push_options[:target] && !@project.repository.branch_exists?(@target)
+ raise Error, "Branch #{@target} does not exist"
+ end
+ 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)
+ Gitlab::ChangesList.new(raw_changes).each_with_object({}) do |change, changes|
+ 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
+
+ changes[branch_name] = change
+ end
+ end
+
+ def merge_requests
+ @merge_requests ||= MergeRequest.from_project(@project)
+ .opened
+ .from_source_branches(@branches)
+ .to_a # fetch now
+ end
+
+ def execute_for_branch(branch)
+ merge_request = merge_requests.find { |mr| mr.source_branch == branch }
+
+ if merge_request
+ update!(merge_request)
+ else
+ create!(branch)
+ end
+ end
+
+ def create!(branch)
+ unless push_options[:create]
+ errors << "A merge_request.create push option is required to create a merge request for branch #{branch}"
+ return
+ end
+
+ merge_request = ::MergeRequests::CreateService.new(
+ project,
+ current_user,
+ create_params(branch)
+ ).execute
+
+ collect_errors_from_merge_request(merge_request) unless merge_request.persisted?
+ end
+
+ def update!(merge_request)
+ return if target == merge_request.target_branch
+
+ merge_request = ::MergeRequests::UpdateService.new(
+ project,
+ current_user,
+ { target_branch: target }
+ ).execute(merge_request)
+
+ collect_errors_from_merge_request(merge_request) unless merge_request.valid?
+ end
+
+ def create_params(branch)
+ change = changes_by_branch.fetch(branch)
+
+ commits = project.repository.commits_between(project.default_branch, change[:newrev])
+ commits = CommitCollection.new(project, commits)
+ commit = commits.without_merge_commits.first
+
+ {
+ assignee: current_user,
+ source_branch: branch,
+ target_branch: target,
+ title: commit&.title&.strip || 'New Merge Request',
+ description: commit&.description&.strip
+ }
+ end
+
+ def collect_errors_from_merge_request(merge_request)
+ errors << merge_request.errors.full_messages.to_sentence
+ end
+ end
+end
diff --git a/changelogs/unreleased/43263-git-push-option-to-create-mr.yml b/changelogs/unreleased/43263-git-push-option-to-create-mr.yml
new file mode 100644
index 00000000000..d50c33da162
--- /dev/null
+++ b/changelogs/unreleased/43263-git-push-option-to-create-mr.yml
@@ -0,0 +1,5 @@
+---
+title: Allow merge requests to be created via git push options
+merge_request: 26752
+author:
+type: added
diff --git a/doc/user/project/merge_requests/index.md b/doc/user/project/merge_requests/index.md
index 7c0380152de..678fc3dd196 100644
--- a/doc/user/project/merge_requests/index.md
+++ b/doc/user/project/merge_requests/index.md
@@ -219,6 +219,39 @@ apply the patches. The target branch can be specified using the
[`/target_branch` quick action](../quick_actions.md). If the source
branch already exists, the patches will be applied on top of it.
+## Git push options
+
+> [Introduced](https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/26752) in GitLab 11.10.
+
+GitLab supports using [Git push options](https://git-scm.com/docs/git-push#Documentation/git-push.txt--oltoptiongt) to create merge requests and set the target
+branch during a push. Note that git push options are only available with
+Git 2.10 or newer.
+
+### Create a new merge request using git push options
+
+To create a new merge request for a branch, use the
+`merge_request.create` push option:
+
+```sh
+git push -o merge_request.create
+```
+
+### Set the target branch of a merge request using git push options
+
+To update an existing merge request's target branch, use the
+`merge_request.target=<branch_name>` push option:
+
+```sh
+git push -o merge_request.target=branch_name
+```
+
+You can also create a merge request and set its target branch at the
+same time using a `-o` flag per push option:
+
+```sh
+git push -o merge_request.create -o merge_request.target=branch_name
+```
+
## Find the merge request that introduced a change
> [Introduced](https://gitlab.com/gitlab-org/gitlab-ce/issues/2383) in GitLab 10.5.
diff --git a/lib/api/helpers/internal_helpers.rb b/lib/api/helpers/internal_helpers.rb
index 3fd824877ae..5014ba51b94 100644
--- a/lib/api/helpers/internal_helpers.rb
+++ b/lib/api/helpers/internal_helpers.rb
@@ -43,6 +43,11 @@ module API
::MergeRequests::GetUrlsService.new(project).execute(params[:changes])
end
+ def push_options_warning(warning)
+ options = Array.wrap(params[:push_options]).map { |p| "'#{p}'" }.join(' ')
+ "Error encountered with push options #{options}: #{warning}"
+ end
+
def redis_ping
result = Gitlab::Redis::SharedState.with { |redis| redis.ping }
diff --git a/lib/api/internal.rb b/lib/api/internal.rb
index 9c7b9146c8f..75202fa953c 100644
--- a/lib/api/internal.rb
+++ b/lib/api/internal.rb
@@ -256,19 +256,41 @@ module API
post '/post_receive' do
status 200
+ output = {} # Messages to gitlab-shell
+ user = identify(params[:identifier])
+ project = Gitlab::GlRepository.parse(params[:gl_repository]).first
+ push_options = Gitlab::PushOptions.new(params[:push_options])
+
PostReceive.perform_async(params[:gl_repository], params[:identifier],
params[:changes], params[:push_options].to_a)
+
+ if (mr_options = push_options.get(:merge_request))
+ begin
+ service = ::MergeRequests::PushOptionsHandlerService.new(
+ project,
+ user,
+ params[:changes],
+ mr_options
+ ).execute
+
+ if service.errors.present?
+ output[:warnings] = push_options_warning(service.errors.join("\n\n"))
+ end
+ rescue ::MergeRequests::PushOptionsHandlerService::Error => e
+ output[:warnings] = push_options_warning(e.message)
+ rescue Gitlab::Access::AccessDeniedError
+ output[:warnings] = push_options_warning('User access was denied')
+ end
+ end
+
broadcast_message = BroadcastMessage.current&.last&.message
reference_counter_decreased = Gitlab::ReferenceCounter.new(params[:gl_repository]).decrease
- output = {
- merge_request_urls: merge_request_urls,
+ output.merge!(
broadcast_message: broadcast_message,
- reference_counter_decreased: reference_counter_decreased
- }
-
- project = Gitlab::GlRepository.parse(params[:gl_repository]).first
- user = identify(params[:identifier])
+ reference_counter_decreased: reference_counter_decreased,
+ merge_request_urls: merge_request_urls
+ )
# A user is not guaranteed to be returned; an orphaned write deploy
# key could be used
diff --git a/lib/gitlab/push_options.rb b/lib/gitlab/push_options.rb
new file mode 100644
index 00000000000..923aa09527d
--- /dev/null
+++ b/lib/gitlab/push_options.rb
@@ -0,0 +1,65 @@
+# frozen_string_literal: true
+
+module Gitlab
+ class PushOptions
+ VALID_OPTIONS = HashWithIndifferentAccess.new({
+ merge_request: {
+ keys: [:create, :target]
+ },
+ ci: {
+ keys: [:skip]
+ }
+ }).freeze
+
+ NAMESPACE_ALIASES = HashWithIndifferentAccess.new({
+ mr: :merge_request
+ }).freeze
+
+ OPTION_MATCHER = /(?<namespace>[^\.]+)\.(?<key>[^=]+)=?(?<value>.*)/
+
+ attr_reader :options
+
+ def initialize(options = [])
+ @options = parse_options(options)
+ end
+
+ def get(*args)
+ options.dig(*args)
+ end
+
+ private
+
+ def parse_options(raw_options)
+ options = HashWithIndifferentAccess.new
+
+ Array.wrap(raw_options).each do |option|
+ namespace, key, value = parse_option(option)
+
+ next if [namespace, key].any?(&:nil?)
+
+ options[namespace] ||= HashWithIndifferentAccess.new
+ options[namespace][key] = value
+ end
+
+ options
+ end
+
+ def parse_option(option)
+ parts = OPTION_MATCHER.match(option)
+ return unless parts
+
+ namespace, key, value = parts.values_at(:namespace, :key, :value).map(&:strip)
+ namespace = NAMESPACE_ALIASES[namespace] if NAMESPACE_ALIASES[namespace]
+ value = value.presence || true
+
+ return unless valid_option?(namespace, key)
+
+ [namespace, key, value]
+ end
+
+ def valid_option?(namespace, key)
+ keys = VALID_OPTIONS.dig(namespace, :keys)
+ keys && keys.include?(key.to_sym)
+ end
+ end
+end
diff --git a/spec/lib/gitlab/push_options_spec.rb b/spec/lib/gitlab/push_options_spec.rb
new file mode 100644
index 00000000000..68b64863c21
--- /dev/null
+++ b/spec/lib/gitlab/push_options_spec.rb
@@ -0,0 +1,91 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe Gitlab::PushOptions do
+ describe 'namespace and key validation' do
+ it 'ignores unrecognised namespaces' do
+ options = described_class.new(['invalid.key=value'])
+
+ expect(options.get(:invalid)).to eq(nil)
+ end
+
+ it 'ignores unrecognised keys' do
+ options = described_class.new(['merge_request.key=value'])
+
+ expect(options.get(:merge_request)).to eq(nil)
+ end
+
+ it 'ignores blank keys' do
+ options = described_class.new(['merge_request'])
+
+ expect(options.get(:merge_request)).to eq(nil)
+ end
+
+ it 'parses recognised namespace and key pairs' do
+ options = described_class.new(['merge_request.target=value'])
+
+ expect(options.get(:merge_request)).to include({
+ target: 'value'
+ })
+ end
+ end
+
+ describe '#get' do
+ it 'can emulate Hash#dig' do
+ options = described_class.new(['merge_request.target=value'])
+
+ expect(options.get(:merge_request, :target)).to eq('value')
+ end
+ end
+
+ it 'can parse multiple push options' do
+ options = described_class.new([
+ 'merge_request.create',
+ 'merge_request.target=value'
+ ])
+
+ expect(options.get(:merge_request)).to include({
+ create: true,
+ target: 'value'
+ })
+ expect(options.get(:merge_request, :create)).to eq(true)
+ expect(options.get(:merge_request, :target)).to eq('value')
+ end
+
+ it 'stores options internally as a HashWithIndifferentAccess' do
+ options = described_class.new([
+ 'merge_request.create'
+ ])
+
+ expect(options.get('merge_request', 'create')).to eq(true)
+ expect(options.get(:merge_request, :create)).to eq(true)
+ end
+
+ it 'selects the last option when options contain duplicate namespace and key pairs' do
+ options = described_class.new([
+ 'merge_request.target=value1',
+ 'merge_request.target=value2'
+ ])
+
+ expect(options.get(:merge_request, :target)).to eq('value2')
+ end
+
+ it 'defaults values to true' do
+ options = described_class.new(['merge_request.create'])
+
+ expect(options.get(:merge_request, :create)).to eq(true)
+ end
+
+ it 'expands aliases' do
+ options = described_class.new(['mr.target=value'])
+
+ expect(options.get(:merge_request, :target)).to eq('value')
+ end
+
+ it 'forgives broken push options' do
+ options = described_class.new(['merge_request . target = value'])
+
+ expect(options.get(:merge_request, :target)).to eq('value')
+ end
+end
diff --git a/spec/requests/api/internal_spec.rb b/spec/requests/api/internal_spec.rb
index 0919540e4ba..62ba4281df5 100644
--- a/spec/requests/api/internal_spec.rb
+++ b/spec/requests/api/internal_spec.rb
@@ -938,6 +938,67 @@ describe API::Internal do
expect(json_response['merge_request_urls']).to eq([])
end
+ it 'does not invoke MergeRequests::PushOptionsHandlerService' do
+ expect(MergeRequests::PushOptionsHandlerService).not_to receive(:new)
+
+ post api("/internal/post_receive"), params: valid_params
+ end
+
+ context 'when there are merge_request push options' do
+ before do
+ valid_params[:push_options] = ['merge_request.create']
+ end
+
+ it 'invokes MergeRequests::PushOptionsHandlerService' do
+ expect(MergeRequests::PushOptionsHandlerService).to receive(:new)
+
+ post api("/internal/post_receive"), params: valid_params
+ end
+
+ it 'links to the newly created merge request' do
+ post api("/internal/post_receive"), params: valid_params
+
+ expect(json_response['merge_request_urls']).to match [{
+ "branch_name" => "new_branch",
+ "url" => "http://#{Gitlab.config.gitlab.host}/#{project.namespace.name}/#{project.path}/merge_requests/1",
+ "new_merge_request" => false
+ }]
+ end
+
+ it 'adds errors raised from MergeRequests::PushOptionsHandlerService to warnings' do
+ expect(MergeRequests::PushOptionsHandlerService).to receive(:new).and_raise(
+ MergeRequests::PushOptionsHandlerService::Error, 'my warning'
+ )
+
+ post api("/internal/post_receive"), params: valid_params
+
+ expect(json_response['warnings']).to eq('Error encountered with push options \'merge_request.create\': my warning')
+ end
+
+ it 'adds errors on the service instance to warnings' do
+ expect_any_instance_of(
+ MergeRequests::PushOptionsHandlerService
+ ).to receive(:errors).at_least(:once).and_return(['my error'])
+
+ post api("/internal/post_receive"), params: valid_params
+
+ expect(json_response['warnings']).to eq('Error encountered with push options \'merge_request.create\': my error')
+ end
+
+ it 'adds ActiveRecord errors on invalid MergeRequest records to warnings' do
+ invalid_merge_request = MergeRequest.new
+ invalid_merge_request.errors.add(:base, 'my error')
+
+ expect_any_instance_of(
+ MergeRequests::CreateService
+ ).to receive(:execute).and_return(invalid_merge_request)
+
+ post api("/internal/post_receive"), params: valid_params
+
+ expect(json_response['warnings']).to eq('Error encountered with push options \'merge_request.create\': my error')
+ end
+ end
+
context 'broadcast message exists' do
let!(:broadcast_message) { create(:broadcast_message, starts_at: 1.day.ago, ends_at: 1.day.from_now ) }
diff --git a/spec/services/merge_requests/push_options_handler_service_spec.rb b/spec/services/merge_requests/push_options_handler_service_spec.rb
new file mode 100644
index 00000000000..96becfcae26
--- /dev/null
+++ b/spec/services/merge_requests/push_options_handler_service_spec.rb
@@ -0,0 +1,291 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe MergeRequests::PushOptionsHandlerService do
+ let(:user) { create(:user) }
+ let(:project) { create(:project, :public, :repository) }
+ let(:service) { described_class.new(project, user, changes, push_options) }
+ let(:source_branch) { 'test' }
+ let(:target_branch) { 'feature' }
+ let(:new_branch_changes) { "#{Gitlab::Git::BLANK_SHA} 570e7b2abdd848b95f2f578043fc23bd6f6fd24d refs/heads/#{source_branch}" }
+ let(:existing_branch_changes) { "d14d6c0abdd253381df51a723d58691b2ee1ab08 570e7b2abdd848b95f2f578043fc23bd6f6fd24d refs/heads/#{source_branch}" }
+ let(:deleted_branch_changes) { "d14d6c0abdd253381df51a723d58691b2ee1ab08 #{Gitlab::Git::BLANK_SHA} refs/heads/#{source_branch}" }
+ let(:default_branch_changes) { "d14d6c0abdd253381df51a723d58691b2ee1ab08 570e7b2abdd848b95f2f578043fc23bd6f6fd24d refs/heads/#{project.default_branch}" }
+
+ before do
+ project.add_developer(user)
+ end
+
+ shared_examples_for 'a service that can create a merge request' do
+ subject(:last_mr) { MergeRequest.last }
+
+ it 'creates a merge request' do
+ expect { service.execute }.to change { MergeRequest.count }.by(1)
+ end
+
+ it 'sets the correct target branch' do
+ branch = push_options[:target] || project.default_branch
+
+ service.execute
+
+ expect(last_mr.target_branch).to eq(branch)
+ end
+
+ it 'assigns the MR to the user' do
+ service.execute
+
+ expect(last_mr.assignee).to eq(user)
+ end
+
+ it 'sets the title and description from the first non-merge commit' do
+ commits = project.repository.commits('master', limit: 5)
+
+ expect(Gitlab::Git::Commit).to receive(:between).at_least(:once).and_return(commits)
+
+ service.execute
+
+ merge_commit = commits.first
+ non_merge_commit = commits.second
+
+ expect(merge_commit.merge_commit?).to eq(true)
+ expect(non_merge_commit.merge_commit?).to eq(false)
+
+ expect(last_mr.title).to eq(non_merge_commit.title)
+ expect(last_mr.description).to eq(non_merge_commit.description)
+ end
+ end
+
+ shared_examples_for 'a service that can set the target of a merge request' do
+ subject(:last_mr) { MergeRequest.last }
+
+ it 'sets the target_branch' do
+ service.execute
+
+ expect(last_mr.target_branch).to eq(target_branch)
+ end
+ end
+
+ shared_examples_for 'a service that does not create a merge request' do
+ it do
+ expect { service.execute }.not_to change { MergeRequest.count }
+ end
+ end
+
+ shared_examples_for 'a service that does not update a merge request' do
+ it do
+ expect { service.execute }.not_to change { MergeRequest.maximum(:updated_at) }
+ end
+ end
+
+ shared_examples_for 'a service that does nothing' do
+ include_examples 'a service that does not create a merge request'
+ include_examples 'a service that does not update a merge request'
+ end
+
+ describe '`create` push option' do
+ let(:push_options) { { create: true } }
+
+ context 'with a new branch' do
+ let(:changes) { new_branch_changes }
+
+ it_behaves_like 'a service that can create a merge request'
+ end
+
+ context 'with an existing branch but no open MR' do
+ let(:changes) { existing_branch_changes }
+
+ it_behaves_like 'a service that can create a merge request'
+ end
+
+ context 'with an existing branch that has a merge request open' do
+ let(:changes) { existing_branch_changes }
+ let!(:merge_request) { create(:merge_request, source_project: project, source_branch: source_branch)}
+
+ it_behaves_like 'a service that does not create a merge request'
+ end
+
+ context 'with a deleted branch' do
+ let(:changes) { deleted_branch_changes }
+
+ it_behaves_like 'a service that does nothing'
+ end
+
+ context 'with the project default branch' do
+ let(:changes) { default_branch_changes }
+
+ it_behaves_like 'a service that does nothing'
+ end
+ end
+
+ describe '`target` push option' do
+ let(:push_options) { { target: target_branch } }
+
+ context 'with a new branch' do
+ let(:changes) { new_branch_changes }
+
+ it_behaves_like 'a service that does not create a merge request'
+
+ it 'adds an error to the service' do
+ error = "A merge_request.create push option is required to create a merge request for branch #{source_branch}"
+
+ service.execute
+
+ expect(service.errors).to include(error)
+ end
+
+ context 'when coupled with the `create` push option' do
+ let(:push_options) { { create: true, target: target_branch } }
+
+ it_behaves_like 'a service that can create a merge request'
+ it_behaves_like 'a service that can set the target of a merge request'
+ end
+ end
+
+ context 'with an existing branch but no open MR' do
+ let(:changes) { existing_branch_changes }
+
+ it_behaves_like 'a service that does not create a merge request'
+
+ it 'adds an error to the service' do
+ error = "A merge_request.create push option is required to create a merge request for branch #{source_branch}"
+
+ service.execute
+
+ expect(service.errors).to include(error)
+ end
+
+ context 'when coupled with the `create` push option' do
+ let(:push_options) { { create: true, target: target_branch } }
+
+ it_behaves_like 'a service that can create a merge request'
+ it_behaves_like 'a service that can set the target of a merge request'
+ end
+ end
+
+ context 'with an existing branch that has a merge request open' do
+ let(:changes) { existing_branch_changes }
+ let!(:merge_request) { create(:merge_request, source_project: project, source_branch: source_branch)}
+
+ it_behaves_like 'a service that does not create a merge request'
+ it_behaves_like 'a service that can set the target of a merge request'
+ end
+
+ context 'with a deleted branch' do
+ let(:changes) { deleted_branch_changes }
+
+ it_behaves_like 'a service that does nothing'
+ end
+
+ context 'with the project default branch' do
+ let(:changes) { default_branch_changes }
+
+ it_behaves_like 'a service that does nothing'
+ end
+ end
+
+ describe 'multiple pushed branches' do
+ let(:push_options) { { create: true } }
+ let(:changes) do
+ [
+ new_branch_changes,
+ "#{Gitlab::Git::BLANK_SHA} 570e7b2abdd848b95f2f578043fc23bd6f6fd24d refs/heads/second-branch"
+ ]
+ end
+
+ it 'creates a merge request per branch' do
+ expect { service.execute }.to change { MergeRequest.count }.by(2)
+ end
+
+ context 'when there are too many pushed branches' do
+ let(:limit) { MergeRequests::PushOptionsHandlerService::LIMIT }
+ let(:changes) do
+ TestEnv::BRANCH_SHA.to_a[0..limit].map do |x|
+ "#{Gitlab::Git::BLANK_SHA} #{x.first} refs/heads/#{x.last}"
+ end
+ end
+
+ it 'throws an error' do
+ expect { service.execute }.to raise_error(
+ MergeRequests::PushOptionsHandlerService::Error,
+ "Too many branches pushed (#{limit + 1} were pushed, limit is #{limit})"
+ )
+ end
+ end
+ end
+
+ describe 'no push options' do
+ let(:push_options) { {} }
+ let(:changes) { new_branch_changes }
+
+ it_behaves_like 'a service that does nothing'
+ end
+
+ describe 'no user' do
+ let(:user) { nil }
+ let(:push_options) { { create: true } }
+ let(:changes) { new_branch_changes }
+
+ it 'throws an error' do
+ expect { service.execute }.to raise_error(
+ MergeRequests::PushOptionsHandlerService::Error,
+ 'User is required'
+ )
+ end
+ end
+
+ describe 'unauthorized user' do
+ let(:push_options) { { create: true } }
+ let(:changes) { new_branch_changes }
+
+ it 'throws an error' do
+ Members::DestroyService.new(user).execute(ProjectMember.find_by!(user_id: user.id))
+
+ expect { service.execute }.to raise_error(Gitlab::Access::AccessDeniedError)
+ end
+ end
+
+ describe 'when target is not a valid branch name' do
+ let(:push_options) { { create: true, target: 'my-branch' } }
+ let(:changes) { new_branch_changes }
+
+ it 'throws an error' do
+ expect { service.execute }.to raise_error(
+ MergeRequests::PushOptionsHandlerService::Error,
+ 'Branch my-branch does not exist'
+ )
+ end
+ end
+
+ describe 'when MRs are not enabled' do
+ let(:push_options) { { create: true } }
+ let(:changes) { new_branch_changes }
+
+ it 'throws an error' do
+ expect(project).to receive(:merge_requests_enabled?).and_return(false)
+
+ expect { service.execute }.to raise_error(
+ MergeRequests::PushOptionsHandlerService::Error,
+ 'Merge requests are not enabled for project'
+ )
+ end
+ end
+
+ describe 'when MR has ActiveRecord errors' do
+ let(:push_options) { { create: true } }
+ let(:changes) { new_branch_changes }
+
+ it 'adds the error to its errors property' do
+ invalid_merge_request = MergeRequest.new
+ invalid_merge_request.errors.add(:base, 'my error')
+
+ expect_any_instance_of(
+ MergeRequests::CreateService
+ ).to receive(:execute).and_return(invalid_merge_request)
+
+ service.execute
+
+ expect(service.errors).to eq(['my error'])
+ end
+ end
+end