diff options
author | Oswaldo Ferreira <oswaldo@gitlab.com> | 2019-05-21 18:14:22 -0300 |
---|---|---|
committer | Oswaldo Ferreira <oswaldo@gitlab.com> | 2019-06-20 11:48:30 -0300 |
commit | 3af348b6cf28ef1d9d3025f7012049132b57798c (patch) | |
tree | 540b724d6b711e3d2247dad5c371b54ec81ea610 /spec | |
parent | c6eb18ee0fe1b887438da87f25fc2f2a852dd393 (diff) | |
download | gitlab-ce-3af348b6cf28ef1d9d3025f7012049132b57798c.tar.gz |
Automatically update MR merge-ref along merge status
This couples the code that transitions the `MergeRequest#merge_status`
and refs/merge-requests/:iid/merge ref update.
In general, instead of directly telling `MergeToRefService` to update
the merge ref, we should rely on `MergeabilityCheckService` to keep
both the merge status and merge ref synced. Now, if the merge_status is
`can_be_merged` it means the merge-ref is also updated to the latest.
We've also updated the logic to be more systematic and less user-based.
Diffstat (limited to 'spec')
-rw-r--r-- | spec/migrations/enqueue_reset_merge_status_second_run_spec.rb | 52 | ||||
-rw-r--r-- | spec/models/merge_request_spec.rb | 87 | ||||
-rw-r--r-- | spec/requests/api/merge_requests_spec.rb | 80 | ||||
-rw-r--r-- | spec/services/merge_requests/merge_to_ref_service_spec.rb | 47 | ||||
-rw-r--r-- | spec/services/merge_requests/mergeability_check_service_spec.rb | 218 | ||||
-rw-r--r-- | spec/services/service_response_spec.rb | 16 |
6 files changed, 358 insertions, 142 deletions
diff --git a/spec/migrations/enqueue_reset_merge_status_second_run_spec.rb b/spec/migrations/enqueue_reset_merge_status_second_run_spec.rb new file mode 100644 index 00000000000..3c880c6f5fd --- /dev/null +++ b/spec/migrations/enqueue_reset_merge_status_second_run_spec.rb @@ -0,0 +1,52 @@ +# frozen_string_literal: true + +require 'spec_helper' +require Rails.root.join('db', 'post_migrate', '20190620112608_enqueue_reset_merge_status_second_run.rb') + +describe EnqueueResetMergeStatusSecondRun, :migration, :sidekiq do + let(:namespaces) { table(:namespaces) } + let(:projects) { table(:projects) } + let(:namespace) { namespaces.create(name: 'gitlab', path: 'gitlab-org') } + let(:project) { projects.create(namespace_id: namespace.id, name: 'foo') } + let(:merge_requests) { table(:merge_requests) } + + def create_merge_request(id, extra_params = {}) + params = { + id: id, + target_project_id: project.id, + target_branch: 'master', + source_project_id: project.id, + source_branch: 'mr name', + title: "mr name#{id}" + }.merge(extra_params) + + merge_requests.create!(params) + end + + it 'correctly schedules background migrations' do + create_merge_request(1, state: 'opened', merge_status: 'can_be_merged') + create_merge_request(2, state: 'opened', merge_status: 'can_be_merged') + create_merge_request(3, state: 'opened', merge_status: 'can_be_merged') + create_merge_request(4, state: 'merged', merge_status: 'can_be_merged') + create_merge_request(5, state: 'opened', merge_status: 'unchecked') + + stub_const("#{described_class.name}::BATCH_SIZE", 2) + + Sidekiq::Testing.fake! do + Timecop.freeze do + migrate! + + expect(described_class::MIGRATION) + .to be_scheduled_delayed_migration(5.minutes, 1, 2) + + expect(described_class::MIGRATION) + .to be_scheduled_delayed_migration(10.minutes, 3, 4) + + expect(described_class::MIGRATION) + .to be_scheduled_delayed_migration(15.minutes, 5, 5) + + expect(BackgroundMigrationWorker.jobs.size).to eq(3) + end + end + end +end diff --git a/spec/models/merge_request_spec.rb b/spec/models/merge_request_spec.rb index c6251326c22..fc28c216b21 100644 --- a/spec/models/merge_request_spec.rb +++ b/spec/models/merge_request_spec.rb @@ -1996,57 +1996,6 @@ describe MergeRequest do end end - describe '#check_if_can_be_merged' do - let(:project) { create(:project, only_allow_merge_if_pipeline_succeeds: true) } - - shared_examples 'checking if can be merged' do - context 'when it is not broken and has no conflicts' do - before do - allow(subject).to receive(:broken?) { false } - allow(project.repository).to receive(:can_be_merged?).and_return(true) - end - - it 'is marked as mergeable' do - expect { subject.check_if_can_be_merged }.to change { subject.merge_status }.to('can_be_merged') - end - end - - context 'when broken' do - before do - allow(subject).to receive(:broken?) { true } - allow(project.repository).to receive(:can_be_merged?).and_return(false) - end - - it 'becomes unmergeable' do - expect { subject.check_if_can_be_merged }.to change { subject.merge_status }.to('cannot_be_merged') - end - end - - context 'when it has conflicts' do - before do - allow(subject).to receive(:broken?) { false } - allow(project.repository).to receive(:can_be_merged?).and_return(false) - end - - it 'becomes unmergeable' do - expect { subject.check_if_can_be_merged }.to change { subject.merge_status }.to('cannot_be_merged') - end - end - end - - context 'when merge_status is unchecked' do - subject { create(:merge_request, source_project: project, merge_status: :unchecked) } - - it_behaves_like 'checking if can be merged' - end - - context 'when merge_status is unchecked' do - subject { create(:merge_request, source_project: project, merge_status: :cannot_be_merged_recheck) } - - it_behaves_like 'checking if can be merged' - end - end - describe '#mergeable?' do let(:project) { create(:project) } @@ -2060,7 +2009,7 @@ describe MergeRequest do it 'return true if #mergeable_state? is true and the MR #can_be_merged? is true' do allow(subject).to receive(:mergeable_state?) { true } - expect(subject).to receive(:check_if_can_be_merged) + expect(subject).to receive(:check_mergeability) expect(subject).to receive(:can_be_merged?) { true } expect(subject.mergeable?).to be_truthy @@ -2074,7 +2023,7 @@ describe MergeRequest do it 'checks if merge request can be merged' do allow(subject).to receive(:mergeable_ci_state?) { true } - expect(subject).to receive(:check_if_can_be_merged) + expect(subject).to receive(:check_mergeability) subject.mergeable? end @@ -3142,38 +3091,6 @@ describe MergeRequest do end end - describe '#mergeable_to_ref?' do - it 'returns true when merge request is mergeable' do - subject = create(:merge_request) - - expect(subject.mergeable_to_ref?).to be(true) - end - - it 'returns false when merge request is already merged' do - subject = create(:merge_request, :merged) - - expect(subject.mergeable_to_ref?).to be(false) - end - - it 'returns false when merge request is closed' do - subject = create(:merge_request, :closed) - - expect(subject.mergeable_to_ref?).to be(false) - end - - it 'returns false when merge request is work in progress' do - subject = create(:merge_request, title: 'WIP: The feature') - - expect(subject.mergeable_to_ref?).to be(false) - end - - it 'returns false when merge request has no commits' do - subject = create(:merge_request, source_branch: 'empty-branch', target_branch: 'master') - - expect(subject.mergeable_to_ref?).to be(false) - end - end - describe '#merge_participants' do it 'contains author' do expect(subject.merge_participants).to eq([subject.author]) diff --git a/spec/requests/api/merge_requests_spec.rb b/spec/requests/api/merge_requests_spec.rb index 9f9180bc8c9..76d093e0774 100644 --- a/spec/requests/api/merge_requests_spec.rb +++ b/spec/requests/api/merge_requests_spec.rb @@ -1546,52 +1546,80 @@ describe API::MergeRequests do end end - describe "PUT /projects/:id/merge_requests/:merge_request_iid/merge_to_ref" do - let(:pipeline) { create(:ci_pipeline_without_jobs) } + describe "GET /projects/:id/merge_requests/:merge_request_iid/merge_ref" do + before do + merge_request.mark_as_unchecked! + end + + let(:merge_request_iid) { merge_request.iid } + let(:url) do - "/projects/#{project.id}/merge_requests/#{merge_request.iid}/merge_to_ref" + "/projects/#{project.id}/merge_requests/#{merge_request_iid}/merge_ref" end it 'returns the generated ID from the merge service in case of success' do - put api(url, user), params: { merge_commit_message: 'Custom message' } - - commit = project.commit(json_response['commit_id']) + get api(url, user) expect(response).to have_gitlab_http_status(200) - expect(json_response['commit_id']).to be_present - expect(commit.message).to eq('Custom message') + expect(json_response['commit_id']).to eq(merge_request.merge_ref_head.sha) end - it "returns 400 if branch can't be merged" do - merge_request.update!(state: 'merged') + context 'when merge-ref is not synced with merge status' do + before do + merge_request.update!(merge_status: 'cannot_be_merged') + end - put api(url, user) + it 'returns 200 if MR can be merged' do + get api(url, user) - expect(response).to have_gitlab_http_status(400) - expect(json_response['message']) - .to eq("Merge request is not mergeable to #{merge_request.merge_ref_path}") + expect(response).to have_gitlab_http_status(200) + expect(json_response['commit_id']).to eq(merge_request.merge_ref_head.sha) + end + + it 'returns 400 if MR cannot be merged' do + expect_next_instance_of(MergeRequests::MergeToRefService) do |merge_request| + expect(merge_request).to receive(:execute) { { status: :failed } } + end + + get api(url, user) + + expect(response).to have_gitlab_http_status(400) + expect(json_response['message']).to eq('Merge request is not mergeable') + end end - it 'returns 403 if user has no permissions to merge to the ref' do - user2 = create(:user) - project.add_reporter(user2) + context 'when user has no access to the MR' do + let(:project) { create(:project, :private) } + let(:merge_request) { create(:merge_request, source_project: project, target_project: project) } - put api(url, user2) + it 'returns 404' do + project.add_guest(user) - expect(response).to have_gitlab_http_status(403) - expect(json_response['message']).to eq('403 Forbidden') + get api(url, user) + + expect(response).to have_gitlab_http_status(404) + expect(json_response['message']).to eq('404 Not found') + end end - it 'returns 404 for an invalid merge request IID' do - put api("/projects/#{project.id}/merge_requests/12345/merge_to_ref", user) + context 'when invalid merge request IID' do + let(:merge_request_iid) { '12345' } - expect(response).to have_gitlab_http_status(404) + it 'returns 404' do + get api(url, user) + + expect(response).to have_gitlab_http_status(404) + end end - it "returns 404 if the merge request id is used instead of iid" do - put api("/projects/#{project.id}/merge_requests/#{merge_request.id}/merge", user) + context 'when merge request ID is used instead IID' do + let(:merge_request_iid) { merge_request.id } - expect(response).to have_gitlab_http_status(404) + it 'returns 404' do + get api(url, user) + + expect(response).to have_gitlab_http_status(404) + end end end diff --git a/spec/services/merge_requests/merge_to_ref_service_spec.rb b/spec/services/merge_requests/merge_to_ref_service_spec.rb index 0ac23050caf..61f99f82a76 100644 --- a/spec/services/merge_requests/merge_to_ref_service_spec.rb +++ b/spec/services/merge_requests/merge_to_ref_service_spec.rb @@ -72,10 +72,6 @@ describe MergeRequests::MergeToRefService do let(:merge_request) { create(:merge_request, :simple) } let(:project) { merge_request.project } - before do - project.add_maintainer(user) - end - describe '#execute' do let(:service) do described_class.new(project, user, commit_message: 'Awesome message', @@ -92,6 +88,12 @@ describe MergeRequests::MergeToRefService do it_behaves_like 'successfully evaluates pre-condition checks' context 'commit history comparison with regular MergeService' do + before do + # The merge service needs an authorized user while merge-to-ref + # doesn't. + project.add_maintainer(user) + end + let(:merge_ref_service) do described_class.new(project, user, {}) end @@ -104,12 +106,18 @@ describe MergeRequests::MergeToRefService do it_behaves_like 'MergeService for target ref' end - context 'when merge commit with squash', :quarantine do + context 'when merge commit with squash' do before do - merge_request.update!(squash: true, source_branch: 'master', target_branch: 'feature') + merge_request.update!(squash: true) end it_behaves_like 'MergeService for target ref' + + it 'does not squash before merging' do + expect(MergeRequests::SquashService).not_to receive(:new) + + process_merge_to_ref + end end end @@ -136,9 +144,9 @@ describe MergeRequests::MergeToRefService do let(:merge_method) { :merge } it 'returns error' do - allow(merge_request).to receive(:mergeable_to_ref?) { false } + allow(project).to receive_message_chain(:repository, :merge_to_ref) { nil } - error_message = "Merge request is not mergeable to #{merge_request.merge_ref_path}" + error_message = 'Conflicts detected during merge' result = service.execute(merge_request) @@ -170,28 +178,5 @@ describe MergeRequests::MergeToRefService do it { expect(todo).not_to be_done } end - - context 'when merge request is WIP state' do - it 'fails to merge' do - merge_request = create(:merge_request, title: 'WIP: The feature') - - result = service.execute(merge_request) - - expect(result[:status]).to eq(:error) - expect(result[:message]).to eq("Merge request is not mergeable to #{merge_request.merge_ref_path}") - end - end - - it 'returns error when user has no authorization to admin the merge request' do - unauthorized_user = create(:user) - project.add_reporter(unauthorized_user) - - service = described_class.new(project, unauthorized_user) - - result = service.execute(merge_request) - - expect(result[:status]).to eq(:error) - expect(result[:message]).to eq('You are not allowed to merge to this ref') - end end end diff --git a/spec/services/merge_requests/mergeability_check_service_spec.rb b/spec/services/merge_requests/mergeability_check_service_spec.rb new file mode 100644 index 00000000000..c5fe51b79f2 --- /dev/null +++ b/spec/services/merge_requests/mergeability_check_service_spec.rb @@ -0,0 +1,218 @@ +# frozen_string_literal: true + +require 'spec_helper' + +describe MergeRequests::MergeabilityCheckService do + shared_examples_for 'unmergeable merge request' do + it 'updates or keeps merge status as cannot_be_merged' do + subject + + expect(merge_request.merge_status).to eq('cannot_be_merged') + end + + it 'does not change the merge ref HEAD' do + expect { subject }.not_to change(merge_request, :merge_ref_head) + end + + it 'returns ServiceResponse.error' do + result = subject + + expect(result).to be_a(ServiceResponse) + expect(result).to be_error + end + end + + shared_examples_for 'mergeable merge request' do + it 'updates or keeps merge status as can_be_merged' do + subject + + expect(merge_request.merge_status).to eq('can_be_merged') + end + + it 'updates the merge ref' do + expect { subject }.to change(merge_request, :merge_ref_head).from(nil) + end + + it 'returns ServiceResponse.success' do + result = subject + + expect(result).to be_a(ServiceResponse) + expect(result).to be_success + end + + it 'ServiceResponse has merge_ref_head payload' do + result = subject + + expect(result.payload.keys).to contain_exactly(:merge_ref_head) + expect(result.payload[:merge_ref_head].keys) + .to contain_exactly(:commit_id, :target_id, :source_id) + end + end + + describe '#execute' do + let(:project) { create(:project, :repository) } + let(:merge_request) { create(:merge_request, merge_status: :unchecked, source_project: project, target_project: project) } + let(:repo) { project.repository } + + subject { described_class.new(merge_request).execute } + + before do + project.add_developer(merge_request.author) + end + + it_behaves_like 'mergeable merge request' + + context 'when multiple calls to the service' do + it 'returns success' do + subject + result = subject + + expect(result).to be_a(ServiceResponse) + expect(result.success?).to be(true) + end + + it 'second call does not change the merge-ref' do + expect { subject }.to change(merge_request, :merge_ref_head).from(nil) + expect { subject }.not_to change(merge_request, :merge_ref_head) + end + end + + context 'disabled merge ref sync feature flag' do + before do + stub_feature_flags(merge_ref_auto_sync: false) + end + + it 'returns error and no payload' do + result = subject + + expect(result).to be_a(ServiceResponse) + expect(result.error?).to be(true) + expect(result.message).to eq('Merge ref is outdated due to disabled feature') + expect(result.payload).to be_empty + end + + it 'updates the merge_status' do + expect { subject }.to change(merge_request, :merge_status).from('unchecked').to('can_be_merged') + end + end + + context 'when broken' do + before do + allow(merge_request).to receive(:broken?) { true } + allow(project.repository).to receive(:can_be_merged?) { false } + end + + it_behaves_like 'unmergeable merge request' + + it 'returns ServiceResponse.error' do + result = subject + + expect(result).to be_a(ServiceResponse) + expect(result.error?).to be(true) + expect(result.message).to eq('Merge request is not mergeable') + end + end + + context 'when it has conflicts' do + before do + allow(merge_request).to receive(:broken?) { false } + allow(project.repository).to receive(:can_be_merged?) { false } + end + + it_behaves_like 'unmergeable merge request' + + it 'returns ServiceResponse.error' do + result = subject + + expect(result).to be_a(ServiceResponse) + expect(result.error?).to be(true) + expect(result.message).to eq('Merge request is not mergeable') + end + end + + context 'when MR cannot be merged and has no merge ref' do + before do + merge_request.mark_as_unmergeable! + end + + it_behaves_like 'unmergeable merge request' + + it 'returns ServiceResponse.error' do + result = subject + + expect(result).to be_a(ServiceResponse) + expect(result.error?).to be(true) + expect(result.message).to eq('Merge request is not mergeable') + end + end + + context 'when MR cannot be merged and has outdated merge ref' do + before do + MergeRequests::MergeToRefService.new(project, merge_request.author).execute(merge_request) + merge_request.mark_as_unmergeable! + end + + it_behaves_like 'unmergeable merge request' + + it 'returns ServiceResponse.error' do + result = subject + + expect(result).to be_a(ServiceResponse) + expect(result.error?).to be(true) + expect(result.message).to eq('Merge request is not mergeable') + end + end + + context 'when merge request is not given' do + subject { described_class.new(nil).execute } + + it 'returns ServiceResponse.error' do + result = subject + + expect(result).to be_a(ServiceResponse) + expect(result.message).to eq('Invalid argument') + end + end + + context 'when read only DB' do + it 'returns ServiceResponse.error' do + allow(Gitlab::Database).to receive(:read_only?) { true } + + result = subject + + expect(result).to be_a(ServiceResponse) + expect(result.message).to eq('Unsupported operation') + end + end + + context 'when fails to update the merge-ref' do + before do + expect_next_instance_of(MergeRequests::MergeToRefService) do |merge_to_ref| + expect(merge_to_ref).to receive(:execute).and_return(status: :failed) + end + end + + it_behaves_like 'unmergeable merge request' + + it 'returns ServiceResponse.error' do + result = subject + + expect(result).to be_a(ServiceResponse) + expect(result.error?).to be(true) + expect(result.message).to eq('Merge request is not mergeable') + end + end + + context 'recheck enforced' do + subject { described_class.new(merge_request).execute(recheck: true) } + + context 'when MR is mergeable but merge-ref does not exists' do + before do + merge_request.mark_as_mergeable! + end + + it_behaves_like 'mergeable merge request' + end + end + end +end diff --git a/spec/services/service_response_spec.rb b/spec/services/service_response_spec.rb index 30bd4d6820b..e790d272e61 100644 --- a/spec/services/service_response_spec.rb +++ b/spec/services/service_response_spec.rb @@ -16,6 +16,13 @@ describe ServiceResponse do expect(response).to be_success expect(response.message).to eq('Good orange') end + + it 'creates a successful response with payload' do + response = described_class.success(payload: { good: 'orange' }) + + expect(response).to be_success + expect(response.payload).to eq(good: 'orange') + end end describe '.error' do @@ -33,6 +40,15 @@ describe ServiceResponse do expect(response.message).to eq('Bad apple') expect(response.http_status).to eq(400) end + + it 'creates a failed response with payload' do + response = described_class.error(message: 'Bad apple', + payload: { bad: 'apple' }) + + expect(response).to be_error + expect(response.message).to eq('Bad apple') + expect(response.payload).to eq(bad: 'apple') + end end describe '#success?' do |