Commit ab34a0d4 authored by Kamil Trzciński's avatar Kamil Trzciński

Merge branch 'add-merge-train-auto-merge-strategy' into 'master'

[New Auto Merge Strategy] Merge Train

See merge request gitlab-org/gitlab-ee!13278
parents 745ba70d 0d880dbb
......@@ -54,3 +54,5 @@ class AutoMergeService < BaseService
self.class.get_service_class(strategy)&.new(project, current_user, params)
end
end
AutoMergeService.prepend(EE::AutoMergeService)
......@@ -71,14 +71,6 @@ module EE
end
end
def get_on_train!(user)
create_merge_train!(user: user, target_project: target_project, target_branch: target_branch)
end
def get_off_train!
merge_train.destroy!
end
def on_train?
merge_train.present?
end
......
......@@ -30,6 +30,10 @@ class MergeTrain < ApplicationRecord
self.class.all_in_train(merge_request).where('merge_trains.id > ?', id)
end
def next
all_next.first
end
def index
self.class.all_in_train(merge_request).where('merge_trains.id < ?', id).count
end
......
# frozen_string_literal: true
module AutoMerge
class MergeTrainService < AutoMerge::BaseService
def execute(merge_request)
merge_request.build_merge_train(user: current_user,
target_project: merge_request.target_project,
target_branch: merge_request.target_branch)
super do
SystemNoteService.merge_train(merge_request, project, current_user, merge_request.merge_train)
end
end
def process(merge_request)
return unless merge_request.on_train?
::MergeTrains::RefreshMergeRequestsService.new(project, nil).execute(merge_request)
end
def cancel(merge_request, reason: nil, refresh_next: true)
# Before dropping a merge request from a merge train, get the next
# merge request in order to refresh it later.
next_merge_request = merge_request.merge_train&.next if refresh_next
super(merge_request) do
if merge_request.merge_train&.delete
SystemNoteService.cancel_merge_train(merge_request, project, current_user, reason: reason)
AutoMergeProcessWorker.perform_async(next_merge_request.id) if next_merge_request
end
end
end
def available_for?(merge_request)
return false unless merge_request.project.merge_trains_enabled?
return false if merge_request.for_fork?
return false unless merge_request.actual_head_pipeline&.complete?
return false unless merge_request.mergeable_state?(skip_ci_check: true)
true
end
end
end
# frozen_string_literal: true
module EE
module AutoMergeService
extend ActiveSupport::Concern
STRATEGY_MERGE_TRAIN = 'merge_train'.freeze
EE_STRATEGIES = [STRATEGY_MERGE_TRAIN].freeze
class_methods do
extend ::Gitlab::Utils::Override
include ::Gitlab::Utils::StrongMemoize
override :all_strategies
def all_strategies
strong_memoize(:all_strategies) do
super + EE_STRATEGIES
end
end
end
end
end
......@@ -189,5 +189,26 @@ module EE
def change_epics_relation_act(subject_epic, user, action, text, text_params)
create_note(NoteSummary.new(subject_epic, nil, user, text % text_params, action: action))
end
# Called when 'merge train' is executed
def merge_train(noteable, project, author, merge_train)
index = merge_train.index
body = if index == 0
'started a merge train'
else
"added this merge request to the merge train at index #{index}"
end
create_note(NoteSummary.new(noteable, project, author, body, action: 'merge'))
end
# Called when 'merge train' is canceled
def cancel_merge_train(noteable, project, author, reason: nil)
body = 'removed this merge request from the merge train'
body += " because #{reason}" if reason
create_note(NoteSummary.new(noteable, project, author, body, action: 'merge'))
end
end
end
# frozen_string_literal: true
module MergeTrains
class CreatePipelineService < BaseService
def execute(merge_request)
validation_status = validate(merge_request)
return validation_status unless validation_status[:status] == :success
merge_status = create_merge_ref(merge_request)
return error(merge_status[:message]) unless merge_status[:status] == :success
create_pipeline(merge_request, merge_status)
end
private
def validate(merge_request)
return error('merge trains is disabled') unless merge_request.project.merge_trains_enabled?
return error('merge request is not on a merge train') unless merge_request.on_train?
return error('fork merge request is not supported') if merge_request.for_fork?
success
end
def create_merge_ref(merge_request)
::MergeRequests::MergeToRefService.new(merge_request.project, merge_request.merge_user).execute(merge_request)
end
def create_pipeline(merge_request, merge_status)
pipeline = ::Ci::CreatePipelineService.new(merge_request.source_project, merge_request.merge_user,
ref: merge_request.merge_ref_path,
checkout_sha: merge_status[:commit_id],
target_sha: merge_status[:target_id],
source_sha: merge_status[:source_id])
.execute(:merge_request_event, merge_request: merge_request)
return error(pipeline.errors.full_messages.join(',')) unless pipeline.persisted?
success(pipeline: pipeline)
end
end
end
# frozen_string_literal: true
module MergeTrains
class RefreshMergeRequestService < BaseService
include Gitlab::Utils::StrongMemoize
ProcessError = Class.new(StandardError)
attr_reader :merge_request
##
# Arguments:
# merge_request ... The merge request to be refreshed
def execute(merge_request)
@merge_request = merge_request
validate!
create_pipeline! if should_create_pipeline?
merge! if should_merge?
success
rescue ProcessError => e
drop(e)
end
private
def validate!
unless project.merge_trains_enabled? && project.merge_pipelines_enabled?
raise ProcessError, 'project disabled merge trains'
end
unless merge_request.on_train?
raise ProcessError, 'merge request is not on a merge train'
end
unless merge_request.mergeable_state?(skip_ci_check: true)
raise ProcessError, 'merge request is not mergeable'
end
if pipeline_for_merge_train
if pipeline_for_merge_train.complete? && !pipeline_for_merge_train.success?
raise ProcessError, 'pipeline did not succeed'
end
end
end
def should_create_pipeline?
first_in_train? && (pipeline_absent? || stale_pipeline?)
end
def create_pipeline!
result = MergeTrains::CreatePipelineService.new(merge_request.project, merge_user)
.execute(merge_request)
raise ProcessError, result[:message] unless result[:status] == :success
merge_train.update!(pipeline: result[:pipeline])
end
def should_merge?
first_in_train? && pipeline_for_merge_train&.success?
end
def merge!
MergeRequests::MergeService.new(project, merge_user, merge_request.merge_params)
.execute(merge_request)
raise ProcessError, 'failed to merge' unless merge_request.merged?
merge_train.delete
end
def stale_pipeline?
pipeline_for_merge_train && !pipeline_for_merge_train.latest_merge_request_pipeline?
end
def pipeline_absent?
!pipeline_for_merge_train.present?
end
def merge_train
merge_request.merge_train
end
def pipeline_for_merge_train
merge_train.pipeline
end
def merge_user
merge_request.merge_user
end
def first_in_train?
strong_memoize(:is_first_in_train) do
merge_train.first_in_train?
end
end
def drop(error)
AutoMerge::MergeTrainService.new(project, merge_user)
.cancel(merge_request, reason: error.message, refresh_next: false)
error(error.message)
end
end
end
# frozen_string_literal: true
module MergeTrains
class RefreshMergeRequestsService < BaseService
include ::Gitlab::ExclusiveLeaseHelpers
##
# merge_request ... A merge request pointer in a merge train.
# All the merge requests following the specified merge request will be refreshed.
def execute(merge_request)
return unless merge_request.on_train?
in_lock("merge_train:#{merge_request.target_project_id}-#{merge_request.target_branch}") do
unsafe_refresh(merge_request)
end
end
private
def unsafe_refresh(merge_request)
following_merge_requests_from(merge_request).each do |merge_request|
MergeTrains::RefreshMergeRequestService
.new(merge_request.project, merge_request.merge_user)
.execute(merge_request)
end
end
def following_merge_requests_from(merge_request)
merge_request.merge_train.all_next.to_a.unshift(merge_request)
end
end
end
---
title: Add Merge Train auto merge strategy
merge_request: 13278
author:
type: added
......@@ -13,8 +13,14 @@ FactoryBot.modify do
train_creator { author }
end
auto_merge_enabled true
auto_merge_strategy AutoMergeService::STRATEGY_MERGE_TRAIN
merge_user { train_creator }
after :create do |merge_request, evaluator|
merge_request.get_on_train!(evaluator.train_creator)
merge_request.create_merge_train(user: evaluator.train_creator,
target_project: merge_request.target_project,
target_branch: merge_request.target_branch)
end
end
......
# frozen_string_literal: true
require 'rails_helper'
describe 'Two merge requests on a merge train' do
let(:project) { create(:project, :repository) }
set(:maintainer_1) { create(:user) }
set(:maintainer_2) { create(:user) }
let(:merge_request_1) do
create(:merge_request,
source_branch: 'feature', source_project: project,
target_branch: 'master', target_project: project,
merge_status: 'unchecked')
end
let(:merge_request_2) do
create(:merge_request,
source_branch: 'signed-commits', source_project: project,
target_branch: 'master', target_project: project,
merge_status: 'unchecked')
end
let(:ci_yaml) do
{ test: { stage: 'test', script: 'echo', only: ['merge_requests'] } }
end
before do
project.add_maintainer(maintainer_1)
project.add_maintainer(maintainer_2)
stub_licensed_features(merge_pipelines: true, merge_trains: true)
project.update!(merge_pipelines_enabled: true, merge_trains_enabled: true)
stub_ci_pipeline_yaml_file(YAML.dump(ci_yaml))
head_pipeline = double('Ci::Pipeline')
allow(head_pipeline).to receive(:complete?) { true }
allow(merge_request_1).to receive(:actual_head_pipeline) { head_pipeline }
allow(merge_request_2).to receive(:actual_head_pipeline) { head_pipeline }
AutoMergeService.new(project, maintainer_1)
.execute(merge_request_1, AutoMergeService::STRATEGY_MERGE_TRAIN)
AutoMergeService.new(project, maintainer_2)
.execute(merge_request_2, AutoMergeService::STRATEGY_MERGE_TRAIN)
merge_request_1.reload
merge_request_2.reload
end
it 'creates a pipeline for merge request 1' do
expect(merge_request_1.merge_train.pipeline).to be_merge_request_pipeline
expect(merge_request_1.merge_train.pipeline.user).to eq(maintainer_1)
end
it 'does not create a pipeline for merge request 2' do
expect(merge_request_2.merge_train.pipeline).to be_nil
end
it 'does not merge anything yet' do
expect(merge_request_1).to be_opened
expect(merge_request_2).to be_opened
end
context 'when the pipeline for merge request 1 succeeded' do
before do
merge_request_1.merge_train.pipeline.succeed!
merge_request_1.reload
merge_request_2.reload
end
it 'merges merge request 1' do
expect(merge_request_1).to be_merged
expect(merge_request_1.metrics.merged_by).to eq(maintainer_1)
end
it 'removes merge request 1 from the merge train' do
expect(merge_request_1.merge_train).to be_nil
end
it 'creates a pipeline for merge request 2' do
expect(merge_request_2.merge_train.pipeline).to be_merge_request_pipeline
expect(merge_request_2.merge_train.pipeline.user).to eq(maintainer_2)
end
context 'when the pipeline for merge request 2 succeeded' do
before do
merge_request_2.merge_train.pipeline.succeed!
merge_request_2.reload
end
it 'merges merge request 2' do
expect(merge_request_2).to be_merged
expect(merge_request_2.metrics.merged_by).to eq(maintainer_2)
end
it 'removes merge request 2 from the merge train' do
expect(merge_request_2.merge_train).to be_nil
end
end
end
context 'when the pipeline for merge request 1 failed' do
before do
merge_request_1.merge_train.pipeline.drop!
merge_request_1.reload
merge_request_2.reload
end
it 'does not merges merge request 1' do
expect(merge_request_1).to be_opened
end
it 'drops merge request 1 from the merge train' do
expect(merge_request_1.merge_train).to be_nil
expect(merge_request_1.notes.last.note).to eq('removed this merge request from the merge train because pipeline did not succeed')
end
it 'creates a pipeline for merge request 2' do
expect(merge_request_2.merge_train.pipeline).to be_merge_request_pipeline
expect(merge_request_2.merge_train.pipeline.user).to eq(maintainer_2)
end
end
context 'when merge request 1 is canceled by a user' do
before do
AutoMergeService.new(project, maintainer_1).cancel(merge_request_1)
merge_request_1.reload
merge_request_2.reload
end
it 'drops merge request 1 from the merge train' do
expect(merge_request_1.merge_train).to be_nil
expect(merge_request_1.notes.last.note).to eq('removed this merge request from the merge train')
end
it 'creates a pipeline for merge request 2' do
expect(merge_request_2.merge_train.pipeline).to be_merge_request_pipeline
expect(merge_request_2.merge_train.pipeline.user).to eq(maintainer_2)
end
end
context 'when merge request 1 is not mergeable' do
before do
merge_request_1.update!(title: merge_request_1.wip_title)
merge_request_1.merge_train.pipeline.succeed!
merge_request_1.reload
merge_request_2.reload
end
it 'drops merge request 1 from the merge train' do
expect(merge_request_1.merge_train).to be_nil
expect(merge_request_1.notes.last.note).to eq('removed this merge request from the merge train because merge request is not mergeable')
end
it 'creates a pipeline for merge request 2' do
expect(merge_request_2.merge_train.pipeline).to be_merge_request_pipeline
expect(merge_request_2.merge_train.pipeline.user).to eq(maintainer_2)
end
end
context 'when merge trains option is disabled' do
before do
project.update!(merge_trains_enabled: false)
merge_request_1.merge_train.pipeline.succeed!
merge_request_1.reload
merge_request_2.reload
end
it 'drops merge request 1 from the merge train' do
expect(merge_request_1.merge_train).to be_nil
expect(merge_request_1.notes.last.note).to eq('removed this merge request from the merge train because project disabled merge trains')
end
it 'drops merge request 2 from the merge train' do
expect(merge_request_2.merge_train).to be_nil
expect(merge_request_2.notes.last.note).to eq('removed this merge request from the merge train because project disabled merge trains')
end
after do
project.update!(merge_trains_enabled: true)
end
end
end
......@@ -622,46 +622,6 @@ describe MergeRequest do
end
end
describe '#get_on_train!' do
subject { merge_request.get_on_train!(user) }
let(:user) { create(:user) }
it 'gets on the train' do
expect { subject }.to change { MergeTrain.count }.by(1)
end
context 'when the merge request is already on a merge train' do
before do
merge_request.get_on_train!(user)
end
it 'raises an exception' do
expect { merge_request.get_on_train!(user) }.to raise_exception(ActiveRecord::RecordNotUnique)
end
end
end
describe '#get_off_train!' do
subject { merge_request.get_off_train! }
let!(:merge_request) do
create(:merge_request, :on_train, source_project: project, target_project: project)
end
it 'gets off from the train' do
expect { subject }.to change { MergeTrain.count }.by(-1)
end
context 'when the merge request is not on a merge train yet' do
let(:merge_request) { create(:merge_request, source_project: project, target_project: project) }
it 'raises an exception' do
expect { subject }.to raise_exception(NoMethodError)
end
end
end
describe '#on_train?' do
subject { merge_request.on_train? }
......
......@@ -11,6 +11,10 @@ describe MergeTrain do
it { is_expected.to belong_to(:user) }
it { is_expected.to belong_to(:pipeline) }
before do
allow(AutoMergeProcessWorker).to receive(:perform_async)
end
describe '.all_in_train' do
subject { described_class.all_in_train(merge_request) }
......@@ -108,6 +112,27 @@ describe MergeTrain do
end
end
describe '#next' do
subject { merge_train.next }
let(:merge_train) { merge_request.merge_train }
let!(:merge_request) { create_merge_request_on_train }
context 'when the merge request is at last on the train' do
it 'returns nil' do
is_expected.to be_nil
end
end
context 'when the other merge request is on the merge train' do
let!(:merge_request_2) { create_merge_request_on_train(source_branch: 'improve/awesome') }
it 'returns the next merge request' do
is_expected.to eq(merge_request_2)
end
end
end
describe '#first_in_train?' do
subject { merge_train.first_in_train? }
......
# frozen_string_literal: true
require 'spec_helper'
describe AutoMerge::MergeTrainService do
include ExclusiveLeaseHelpers
set(:project) { create(:project, :repository) }
set(:user) { create(:user) }
let(:service) { described_class.new(project, user, params) }
let(:params) { {} }
let(:merge_request) do
create(:merge_request, :with_merge_request_pipeline,
source_project: project, target_project: project)
end
before do
project.add_maintainer(user)
allow(AutoMergeProcessWorker).to receive(:perform_async) { }
stub_licensed_features(merge_trains: true, merge_pipelines: true)
project.update!(merge_trains_enabled: true, merge_pipelines_enabled: true)
end
describe '#execute' do
subject { service.execute(merge_request) }
it 'enables auto merge on the merge request' do
subject
merge_request.reload
expect(merge_request.auto_merge_enabled).to be_truthy
expect(merge_request.merge_user).to eq(user)
expect(merge_request.auto_merge_strategy).to eq(AutoMergeService::STRATEGY_MERGE_TRAIN)
end
it 'creates merge train' do
subject
merge_request.reload
expect(merge_request.merge_train).to be_present
expect(merge_request.merge_train.user).to eq(user)
end
it 'creates system note' do
expect(SystemNoteService)
.to receive(:merge_train).with(merge_request, project, user, instance_of(MergeTrain))
subject
end
it 'returns result code' do
is_expected.to eq(:merge_train)
end
context 'when failed to save the record' do
before do
allow(merge_request).to receive(:save) { false }
end
it 'returns result code' do
is_expected.to eq(:failed)
end
end
end
describe '#process' do
subject { service.process(merge_request) }
let(:merge_request) do
create(:merge_request, :on_train,
source_project: project, source_branch: 'feature',
target_project: project, target_branch: 'master')
end
it 'calls RefreshMergeRequestsService' do
expect_next_instance_of(MergeTrains::RefreshMergeRequestsService) do |service|
expect(service).to receive(:execute).with(merge_request)
end
subject
end
context 'when merge request is not on a merge train' do
let(:merge_request) { create(:merge_request) }
it 'does not call RefreshMergeRequestsService' do
expect(MergeTrains::RefreshMergeRequestsService).not_to receive(:new)
subject
end
end
end
describe '#cancel' do
subject { service.cancel(merge_request, **params) }
let(:params) { {} }
let!(:merge_request) do
create(:merge_request, :on_train,
source_project: project, source_branch: 'feature',
target_project: project, target_branch: 'master')
end
it 'cancels auto merge on the merge request' do
subject
merge_request.reload
expect(merge_request).not_to be_auto_merge_enabled
expect(merge_request.merge_user).to be_nil
expect(merge_request.merge_params).not_to include('should_remove_source_branch')
expect(merge_request.merge_params).not_to include('commit_message')
expect(merge_request.merge_params).not_to include('squash_commit_message')
expect(merge_request.merge_params).not_to include('auto_merge_strategy')
expect(merge_request.merge_train).not_to be_present
end
it 'writes system note to the merge request' do
expect(SystemNoteService)
.to receive(:cancel_merge_train).with(merge_request, project, user, anything)
subject
end
context 'when reason is specified' do
let(:params) { { reason: 'Pipeline failed' } }
it 'passes the reason to SystemNoteService' do
expect(SystemNoteService)
.to receive(:cancel_merge_train).with(any_args, reason: 'Pipeline failed')
subject
end
end
context 'when the other merge request is following the merge request' do
let!(:merge_request_2) do
create(:merge_request, :on_train,
source_project: project, source_branch: 'signed-commits',
target_project: project, target_branch: 'master')
end
it 'processes the next merge request on the train by default' do
expect(AutoMergeProcessWorker).to receive(:perform_async).with(merge_request_2.id)
subject
end
context 'when refresh next is false' do
let(:params) { { refresh_next: false } }
it 'does not process the next merge request on the train' do
expect(AutoMergeProcessWorker).not_to receive(:perform_async).with(merge_request_2.id)
subject
end
end
end
end
describe '#available_for?' do
subject { service.available_for?(merge_request) }
let(:pipeline) { double }
before do
allow(merge_request).to receive(:mergeable_state?) { true }
allow(merge_request).to receive(:for_fork?) { false }
allow(merge_request).to receive(:actual_head_pipeline) { pipeline }
allow(pipeline).to receive(:complete?) { true }
end
it { is_expected.to be_truthy }
context 'when merge trains project option is disabled' do
before do
project.update!(merge_trains_enabled: false)
end
it { is_expected.to be_falsy }
after do
project.update!(merge_trains_enabled: true)
end
end
context 'when merge request is not mergeable' do
before do
allow(merge_request).to receive(:mergeable_state?) { false }
end
it { is_expected.to be_falsy }
end
context 'when merge request is submitted from a forked project' do
before do
allow(merge_request).to receive(:for_fork?) { true }
end
it { is_expected.to be_falsy }
end
context 'when the head pipeline of the merge request has not finished' do
before do
allow(pipeline).to receive(:complete?) { false }
end
it { is_expected.to be_falsy }
end
end
def create_pipeline_for(merge_request)
MergeRequests::CreatePipelineService.new(project, user).execute(merge_request)
end
end
# frozen_string_literal: true
require 'spec_helper'
describe AutoMergeService do
describe '.all_strategies' do
subject { described_class.all_strategies }
it 'includes all strategies' do
is_expected.to include(AutoMergeService::STRATEGY_MERGE_TRAIN)
end
end
end
# frozen_string_literal: true
require 'spec_helper'
describe MergeTrains::CreatePipelineService do
set(:project) { create(:project, :repository) }
set(:maintainer) { create(:user) }
let(:service) { described_class.new(project, maintainer) }
before do
project.add_maintainer(maintainer)
stub_licensed_features(merge_pipelines: true, merge_trains: true)
project.update!(merge_pipelines_enabled: true, merge_trains_enabled: true)
end
describe '#execute' do
subject { service.execute(merge_request) }
let!(:merge_request) do
create(:merge_request, :on_train, train_creator: maintainer,
source_branch: 'feature', source_project: project,
target_branch: 'master', target_project: project,
merge_status: 'unchecked')
end
shared_examples_for 'returns an error' do
let(:expected_reason) { 'unknown' }
it do
expect(subject[:status]).to eq(:error)
expect(subject[:message]).to eq(expected_reason)
end
end
context 'when merge trains option is disabled' do
before do
project.update!(merge_trains_enabled: false)
end
it_behaves_like 'returns an error' do
let(:expected_reason) { 'merge trains is disabled' }
end
after do
project.update!(merge_trains_enabled: true)
end
end
context 'when merge request is not on a merge train' do
let!(:merge_request) do
create(:merge_request,
source_branch: 'feature', source_project: project,
target_branch: 'master', target_project: project)
end
it_behaves_like 'returns an error' do
let(:expected_reason) { 'merge request is not on a merge train' }
end
end
context 'when merge request is submitted from a forked project' do
before do
allow(merge_request).to receive(:for_fork?) { true }
end
it_behaves_like 'returns an error' do
let(:expected_reason) { 'fork merge request is not supported' }
end
end
context 'when prepared merge ref successfully' do
context 'when .gitlab-ci.yml has only: [merge_requests] specification' do
let(:ci_yaml) do
{ test: { stage: 'test', script: 'echo', only: ['merge_requests'] } }
end
before do
stub_ci_pipeline_yaml_file(YAML.dump(ci_yaml))
end
it 'calls Ci::CreatePipelineService' do
expect_next_instance_of(Ci::CreatePipelineService, project, maintainer, any_args) do |pipeline_service|
expect(pipeline_service).to receive(:execute)
.with(:merge_request_event, hash_including(merge_request: merge_request)).and_call_original
end
subject
end
end
context 'when .gitlab-ci.yml does not have only: [merge_requests] specification' do
it_behaves_like 'returns an error' do
let(:expected_reason) { 'No stages / jobs for this pipeline.' }
end
end
end
context 'when failed to prepare merge ref' do
before do
check_service = double
allow(::MergeRequests::MergeToRefService).to receive(:new) { check_service }
allow(check_service).to receive(:execute) { { status: :error, message: 'Merge ref was not found' } }
end
it_behaves_like 'returns an error' do
let(:expected_reason) { 'Merge ref was not found' }
end
end
end
end
# frozen_string_literal: true
require 'spec_helper'
describe MergeTrains::RefreshMergeRequestService do
set(:project) { create(:project, :repository) }
set(:maintainer) { create(:user) }
let(:service) { described_class.new(project, maintainer) }
before do
project.add_maintainer(maintainer)
stub_licensed_features(merge_pipelines: true, merge_trains: true)
project.update!(merge_pipelines_enabled: true, merge_trains_enabled: true)
end
describe '#execute' do
subject { service.execute(merge_request) }
let!(:merge_request) do
create(:merge_request, :on_train, train_creator: maintainer,
source_branch: 'feature', source_project: project,
target_branch: 'master', target_project: project)
end
shared_examples_for 'drops the merge request from the merge train' do
let(:expected_reason) { 'unknown' }
it do
expect_next_instance_of(AutoMerge::MergeTrainService) do |service|
expect(service).to receive(:cancel).with(merge_request, hash_including(reason: expected_reason))
end
subject
end
end
context 'when merge train project configuration is disabled' do
before do
project.update!(merge_trains_enabled: false)
end
it_behaves_like 'drops the merge request from the merge train' do
let(:expected_reason) { 'project disabled merge trains' }
end
after do
project.update!(merge_trains_enabled: true)
end
end
context 'when merge request is not under a mergeable state' do
before do
merge_request.update!(title: merge_request.wip_title)
end
it_behaves_like 'drops the merge request from the merge train' do
let(:expected_reason) { 'merge request is not mergeable' }
end
end
context 'when pipeline for merge train failed' do
let(:pipeline) { create(:ci_pipeline, :failed) }
before do
merge_request.merge_train.update!(pipeline: pipeline)
end
it_behaves_like 'drops the merge request from the merge train' do
let(:expected_reason) { 'pipeline did not succeed' }
end
end
context 'when pipeline has not been created yet' do
context 'when the merge request is the first queue' do
it 'creates a pipeline for merge train' do
expect_next_instance_of(MergeTrains::CreatePipelineService, project, maintainer) do |pipeline_service|
expect(pipeline_service).to receive(:execute).with(merge_request).and_call_original
end
subject
end
context 'when it failed to create a pipeline' do
before do
allow_any_instance_of(MergeTrains::CreatePipelineService).to receive(:execute) { { result: :error, message: 'failed to create pipeline' } }
end
it_behaves_like 'drops the merge request from the merge train' do
let(:expected_reason) { 'failed to create pipeline' }
end
end
end
context 'when the merge request is not the first queue' do
before do
allow(merge_request.merge_train).to receive(:first_in_train?) { false }
end
it 'does not create a pipeline for merge train' do
expect(MergeTrains::CreatePipelineService).not_to receive(:new)
subject
end
end
end
context 'when pipeline for merge train succeeded' do
let(:pipeline) { create(:ci_pipeline, :success) }
before do
allow(pipeline).to receive(:latest_merge_request_pipeline?) { true }
merge_request.merge_train.update!(pipeline: pipeline)
end
context 'when the merge request is the first queue' do
it 'merges the merge request' do
expect_next_instance_of(MergeRequests::MergeService, project, maintainer, anything) do |service|
expect(service).to receive(:execute).with(merge_request)
end
subject
end
context 'when it failed to merge the merge request' do
before do
allow_any_instance_of(MergeRequests::MergeService).to receive(:execute) { { result: :error } }
end
it_behaves_like 'drops the merge request from the merge train' do
let(:expected_reason) { 'failed to merge' }
end
end
end
context 'when the merge request is not the first queue' do
before do
allow(merge_request.merge_train).to receive(:first_in_train?) { false }
end
it 'does not merge the merge request' do
expect(MergeRequests::MergeService).not_to receive(:new)
subject
end
end
end
end
end
# frozen_string_literal: true
require 'spec_helper'
describe MergeTrains::RefreshMergeRequestsService do
include ExclusiveLeaseHelpers
set(:project) { create(:project) }
set(:maintainer_1) { create(:user) }
set(:maintainer_2) { create(:user) }
let(:service) { described_class.new(project, maintainer_1) }
before do
project.add_maintainer(maintainer_1)
project.add_maintainer(maintainer_2)
end
describe '#execute' do
subject { service.execute(merge_request) }
let!(:merge_request_1) do
create(:merge_request, :on_train, train_creator: maintainer_1,
source_branch: 'feature', source_project: project,
target_branch: 'master', target_project: project)
end
let!(:merge_request_2) do
create(:merge_request, :on_train, train_creator: maintainer_2,
source_branch: 'signed-commits', source_project: project,
target_branch: 'master', target_project: project)
end
let(:refresh_service_1) { double }
let(:refresh_service_2) { double }
before do
allow(MergeTrains::RefreshMergeRequestService)
.to receive(:new).with(project, maintainer_1) { refresh_service_1 }
allow(MergeTrains::RefreshMergeRequestService)
.to receive(:new).with(project, maintainer_2) { refresh_service_2 }
end
context 'when merge request 1 is passed' do
let(:merge_request) { merge_request_1 }
it 'executes RefreshMergeRequestService to all the following merge requests' do
expect(refresh_service_1).to receive(:execute).with(merge_request_1)
expect(refresh_service_2).to receive(:execute).with(merge_request_2)
subject
end
context 'when merge request 1 is not on a merge train' do
let(:merge_request) { merge_request_1 }
let!(:merge_request_1) { create(:merge_request) }
it 'does not refresh' do
expect(refresh_service_1).not_to receive(:execute).with(merge_request_1)
subject
end
end
context 'when the exlusive lock has already been taken' do
let(:lease_key) do
"merge_train:#{merge_request_1.target_project_id}-#{merge_request_1.target_branch}"
end
before do
stub_exclusive_lease_taken(lease_key)
end
it 'raises FailedToObtainLockError' do
expect { subject }.to raise_error(Gitlab::ExclusiveLeaseHelpers::FailedToObtainLockError)
end
end
end
context 'when merge request 2 is passed' do
let(:merge_request) { merge_request_2 }
it 'executes RefreshMergeRequestService to all the following merge requests' do
expect(refresh_service_1).not_to receive(:execute).with(merge_request_1)
expect(refresh_service_2).to receive(:execute).with(merge_request_2)
subject
end
end
end
end
......@@ -323,4 +323,51 @@ describe SystemNoteService do
end
end
end
describe '.merge_train' do
subject { described_class.merge_train(noteable, project, author, noteable.merge_train) }
let(:noteable) { create(:merge_request, :on_train, source_project: project, target_project: project) }
it_behaves_like 'a system note' do
let(:action) { 'merge' }
end
it "posts the 'merge train' system note" do
expect(subject.note).to eq('started a merge train')
end
context 'when index of the merge request is not zero' do
before do
allow(noteable.merge_train).to receive(:index) { 1 }
end
it "posts the 'merge train' system note" do
expect(subject.note).to eq('added this merge request to the merge train at index 1')
end
end
end
describe '.cancel_merge_train' do
subject { described_class.cancel_merge_train(noteable, project, author, reason: reason) }
let(:noteable) { create(:merge_request, :on_train, source_project: project, target_project: project) }
let(:reason) { }
it_behaves_like 'a system note' do
let(:action) { 'merge' }
end
it "posts the 'merge train' system note" do
expect(subject.note).to eq('removed this merge request from the merge train')
end
context 'when reason is specified' do
let(:reason) { 'merge request is not mergeable' }
it "posts the 'merge train' system note" do
expect(subject.note).to eq('removed this merge request from the merge train because merge request is not mergeable')
end
end
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