Commit 7821defa authored by Douwe Maan's avatar Douwe Maan

Merge branch 'sync-merge-ref-upon-mergeability-check' into 'master'

Automatically update MR merge-ref along merge status

See merge request gitlab-org/gitlab-ce!29569
parents 833cb6e9 74a3e6b7
...@@ -33,7 +33,7 @@ class Projects::MergeRequestsController < Projects::MergeRequests::ApplicationCo ...@@ -33,7 +33,7 @@ class Projects::MergeRequestsController < Projects::MergeRequests::ApplicationCo
def show def show
close_merge_request_if_no_source_project close_merge_request_if_no_source_project
mark_merge_request_mergeable @merge_request.check_mergeability
respond_to do |format| respond_to do |format|
format.html do format.html do
...@@ -251,10 +251,6 @@ class Projects::MergeRequestsController < Projects::MergeRequests::ApplicationCo ...@@ -251,10 +251,6 @@ class Projects::MergeRequestsController < Projects::MergeRequests::ApplicationCo
@merge_request.has_no_commits? && !@merge_request.target_branch_exists? @merge_request.has_no_commits? && !@merge_request.target_branch_exists?
end end
def mark_merge_request_mergeable
@merge_request.check_if_can_be_merged
end
def merge! def merge!
# Disable the CI check if auto_merge_strategy is specified since we have # Disable the CI check if auto_merge_strategy is specified since we have
# to wait until CI completes to know # to wait until CI completes to know
......
...@@ -725,19 +725,16 @@ class MergeRequest < ApplicationRecord ...@@ -725,19 +725,16 @@ class MergeRequest < ApplicationRecord
MergeRequests::ReloadDiffsService.new(self, current_user).execute MergeRequests::ReloadDiffsService.new(self, current_user).execute
end end
# rubocop: enable CodeReuse/ServiceClass
def check_if_can_be_merged
return unless self.class.state_machines[:merge_status].check_state?(merge_status) && Gitlab::Database.read_write?
can_be_merged = def check_mergeability
!broken? && project.repository.can_be_merged?(diff_head_sha, target_branch) MergeRequests::MergeabilityCheckService.new(self).execute
end
# rubocop: enable CodeReuse/ServiceClass
if can_be_merged # Returns boolean indicating the merge_status should be rechecked in order to
mark_as_mergeable # switch to either can_be_merged or cannot_be_merged.
else def recheck_merge_status?
mark_as_unmergeable self.class.state_machines[:merge_status].check_state?(merge_status)
end
end end
def merge_event def merge_event
...@@ -763,7 +760,7 @@ class MergeRequest < ApplicationRecord ...@@ -763,7 +760,7 @@ class MergeRequest < ApplicationRecord
def mergeable?(skip_ci_check: false) def mergeable?(skip_ci_check: false)
return false unless mergeable_state?(skip_ci_check: skip_ci_check) return false unless mergeable_state?(skip_ci_check: skip_ci_check)
check_if_can_be_merged check_mergeability
can_be_merged? && !should_be_rebased? can_be_merged? && !should_be_rebased?
end end
...@@ -778,15 +775,6 @@ class MergeRequest < ApplicationRecord ...@@ -778,15 +775,6 @@ class MergeRequest < ApplicationRecord
true true
end end
def mergeable_to_ref?
return false unless mergeable_state?(skip_ci_check: true, skip_discussions_check: true)
# Given the `merge_ref_path` will have the same
# state the `target_branch` would have. Ideally
# we need to check if it can be merged to it.
project.repository.can_be_merged?(diff_head_sha, target_branch)
end
def ff_merge_possible? def ff_merge_possible?
project.repository.ancestor?(target_branch_sha, diff_head_sha) project.repository.ancestor?(target_branch_sha, diff_head_sha)
end end
...@@ -1099,6 +1087,12 @@ class MergeRequest < ApplicationRecord ...@@ -1099,6 +1087,12 @@ class MergeRequest < ApplicationRecord
target_project.repository.fetch_source_branch!(source_project.repository, source_branch, ref_path) target_project.repository.fetch_source_branch!(source_project.repository, source_branch, ref_path)
end end
# Returns the current merge-ref HEAD commit.
#
def merge_ref_head
project.repository.commit(merge_ref_path)
end
def ref_path def ref_path
"refs/#{Repository::REF_MERGE_REQUEST}/#{iid}/head" "refs/#{Repository::REF_MERGE_REQUEST}/#{iid}/head"
end end
......
...@@ -11,6 +11,8 @@ module MergeRequests ...@@ -11,6 +11,8 @@ module MergeRequests
# be executed regardless of the `target_ref` current state). # be executed regardless of the `target_ref` current state).
# #
class MergeToRefService < MergeRequests::MergeBaseService class MergeToRefService < MergeRequests::MergeBaseService
extend ::Gitlab::Utils::Override
def execute(merge_request) def execute(merge_request)
@merge_request = merge_request @merge_request = merge_request
...@@ -26,14 +28,18 @@ module MergeRequests ...@@ -26,14 +28,18 @@ module MergeRequests
success(commit_id: commit.id, success(commit_id: commit.id,
target_id: target_id, target_id: target_id,
source_id: source_id) source_id: source_id)
rescue MergeError => error rescue MergeError, ArgumentError => error
error(error.message) error(error.message)
end end
private private
override :source
def source
merge_request.diff_head_sha
end
def validate! def validate!
authorization_check!
error_check! error_check!
end end
...@@ -43,21 +49,13 @@ module MergeRequests ...@@ -43,21 +49,13 @@ module MergeRequests
error = error =
if !hooks_validation_pass?(merge_request) if !hooks_validation_pass?(merge_request)
hooks_validation_error(merge_request) hooks_validation_error(merge_request)
elsif !@merge_request.mergeable_to_ref? elsif source.blank?
"Merge request is not mergeable to #{target_ref}"
elsif !source
'No source for merge' 'No source for merge'
end end
raise_error(error) if error raise_error(error) if error
end end
def authorization_check!
unless Ability.allowed?(current_user, :admin_merge_request, project)
raise_error("You are not allowed to merge to this ref")
end
end
def target_ref def target_ref
merge_request.merge_ref_path merge_request.merge_ref_path
end end
......
# frozen_string_literal: true
module MergeRequests
class MergeabilityCheckService < ::BaseService
include Gitlab::Utils::StrongMemoize
delegate :project, to: :@merge_request
delegate :repository, to: :project
def initialize(merge_request)
@merge_request = merge_request
end
# Updates the MR merge_status. Whenever it switches to a can_be_merged state,
# the merge-ref is refreshed.
#
# recheck - When given, it'll enforce a merge-ref refresh if the current merge_status is
# can_be_merged or cannot_be_merged and merge-ref is outdated.
# Given MergeRequests::RefreshService is called async, it might happen that the target
# branch gets updated, but the MergeRequest#merge_status lags behind. So in scenarios
# where we need the current state of the merge ref in repository, the `recheck`
# argument is required.
#
# Returns a ServiceResponse indicating merge_status is/became can_be_merged
# and the merge-ref is synced. Success in case of being/becoming mergeable,
# error otherwise.
def execute(recheck: false)
return ServiceResponse.error(message: 'Invalid argument') unless merge_request
return ServiceResponse.error(message: 'Unsupported operation') if Gitlab::Database.read_only?
recheck! if recheck
update_merge_status
unless merge_request.can_be_merged?
return ServiceResponse.error(message: 'Merge request is not mergeable')
end
unless merge_ref_auto_sync_enabled?
return ServiceResponse.error(message: 'Merge ref is outdated due to disabled feature')
end
unless payload.fetch(:merge_ref_head)
return ServiceResponse.error(message: 'Merge ref cannot be updated')
end
ServiceResponse.success(payload: payload)
end
private
attr_reader :merge_request
def payload
strong_memoize(:payload) do
{
merge_ref_head: merge_ref_head_payload
}
end
end
def merge_ref_head_payload
commit = merge_request.merge_ref_head
return unless commit
target_id, source_id = commit.parent_ids
{
commit_id: commit.id,
source_id: source_id,
target_id: target_id
}
end
def update_merge_status
return unless merge_request.recheck_merge_status?
if can_git_merge? && merge_to_ref
merge_request.mark_as_mergeable
else
merge_request.mark_as_unmergeable
end
end
def recheck!
if !merge_request.recheck_merge_status? && outdated_merge_ref?
merge_request.mark_as_unchecked
end
end
# Checks if the existing merge-ref is synced with the target branch.
#
# Returns true if the merge-ref does not exists or is out of sync.
def outdated_merge_ref?
return false unless merge_ref_auto_sync_enabled?
return false unless merge_request.open?
return true unless ref_head = merge_request.merge_ref_head
return true unless target_sha = merge_request.target_branch_sha
return true unless source_sha = merge_request.source_branch_sha
ref_head.parent_ids != [target_sha, source_sha]
end
def can_git_merge?
!merge_request.broken? && repository.can_be_merged?(merge_request.diff_head_sha, merge_request.target_branch)
end
def merge_to_ref
return true unless merge_ref_auto_sync_enabled?
result = MergeRequests::MergeToRefService.new(project, merge_request.author).execute(merge_request)
result[:status] == :success
end
def merge_ref_auto_sync_enabled?
Feature.enabled?(:merge_ref_auto_sync, project, default_enabled: true)
end
end
end
# frozen_string_literal: true # frozen_string_literal: true
class ServiceResponse class ServiceResponse
def self.success(message: nil) def self.success(message: nil, payload: {})
new(status: :success, message: message) new(status: :success, message: message, payload: payload)
end end
def self.error(message:, http_status: nil) def self.error(message:, payload: {}, http_status: nil)
new(status: :error, message: message, http_status: http_status) new(status: :error, message: message, payload: payload, http_status: http_status)
end end
attr_reader :status, :message, :http_status attr_reader :status, :message, :http_status, :payload
def initialize(status:, message: nil, http_status: nil) def initialize(status:, message: nil, payload: {}, http_status: nil)
self.status = status self.status = status
self.message = message self.message = message
self.payload = payload
self.http_status = http_status self.http_status = http_status
end end
...@@ -27,5 +28,5 @@ class ServiceResponse ...@@ -27,5 +28,5 @@ class ServiceResponse
private private
attr_writer :status, :message, :http_status attr_writer :status, :message, :http_status, :payload
end end
---
title: Sync merge ref upon mergeability check
merge_request: 29569
author:
type: added
# frozen_string_literal: true
# See http://doc.gitlab.com/ce/development/migration_style_guide.html
# for more information on how to write migrations for GitLab.
class EnqueueResetMergeStatusSecondRun < ActiveRecord::Migration[5.1]
include Gitlab::Database::MigrationHelpers
DOWNTIME = false
BATCH_SIZE = 10_000
MIGRATION = 'ResetMergeStatus'
DELAY_INTERVAL = 5.minutes.to_i
disable_ddl_transaction!
def up
say 'Scheduling `ResetMergeStatus` jobs'
# We currently have more than ~5_000_000 merge request records on GitLab.com.
# This means it'll schedule ~500 jobs (10k MRs each) with a 5 minutes gap,
# so this should take ~41 hours for all background migrations to complete.
# ((5_000_000 / 10_000) * 5) / 60 => 41.6666..
queue_background_migration_jobs_by_range_at_intervals(MergeRequest, MIGRATION, DELAY_INTERVAL, batch_size: BATCH_SIZE)
end
end
...@@ -10,7 +10,7 @@ ...@@ -10,7 +10,7 @@
# #
# It's strongly recommended that you check this file into your version control system. # It's strongly recommended that you check this file into your version control system.
ActiveRecord::Schema.define(version: 20190619175843) do ActiveRecord::Schema.define(version: 20190620112608) do
# These are extensions that must be enabled in order to support this database # These are extensions that must be enabled in order to support this database
enable_extension "plpgsql" enable_extension "plpgsql"
......
...@@ -1191,33 +1191,29 @@ Parameters: ...@@ -1191,33 +1191,29 @@ Parameters:
} }
``` ```
## Merge to default merge ref path ## Returns the up to date merge-ref HEAD commit
Merge the changes between the merge request source and target branches into `refs/merge-requests/:iid/merge` Merge the changes between the merge request source and target branches into `refs/merge-requests/:iid/merge`
ref, of the target project repository. This ref will have the state the target branch would have if ref, of the target project repository, if possible. This ref will have the state the target branch would have if
a regular merge action was taken. a regular merge action was taken.
This is not a regular merge action given it doesn't change the merge request state in any manner. This is not a regular merge action given it doesn't change the merge request target branch state in any manner.
This ref (`refs/merge-requests/:iid/merge`) is **always** overwritten when submitting This ref (`refs/merge-requests/:iid/merge`) isn't necessarily overwritten when submitting
requests to this API, so none of its state is kept or used in the process. requests to this API, though it'll make sure the ref has the latest possible state.
If the merge request has conflicts, is empty or already merged, If the merge request has conflicts, is empty or already merged, you'll get a `400` and a descriptive error message.
you'll get a `400` and a descriptive error message. If you don't have permissions to do so,
you'll get a `403`.
It returns the HEAD commit of `refs/merge-requests/:iid/merge` in the response body in It returns the HEAD commit of `refs/merge-requests/:iid/merge` in the response body in case of `200`.
case of `200`.
``` ```
PUT /projects/:id/merge_requests/:merge_request_iid/merge_to_ref GET /projects/:id/merge_requests/:merge_request_iid/merge_ref
``` ```
Parameters: Parameters:
- `id` (required) - The ID or [URL-encoded path of the project](README.md#namespaced-path-encoding) owned by the authenticated user - `id` (required) - The ID or [URL-encoded path of the project](README.md#namespaced-path-encoding) owned by the authenticated user
- `merge_request_iid` (required) - Internal ID of MR - `merge_request_iid` (required) - Internal ID of MR
- `merge_commit_message` (optional) - Custom merge commit message
```json ```json
{ {
......
...@@ -703,7 +703,7 @@ module API ...@@ -703,7 +703,7 @@ module API
# See https://gitlab.com/gitlab-org/gitlab-ce/issues/42344 for more # See https://gitlab.com/gitlab-org/gitlab-ce/issues/42344 for more
# information. # information.
expose :merge_status do |merge_request| expose :merge_status do |merge_request|
merge_request.check_if_can_be_merged merge_request.check_mergeability
merge_request.merge_status merge_request.merge_status
end end
expose :diff_head_sha, as: :sha expose :diff_head_sha, as: :sha
......
...@@ -397,28 +397,16 @@ module API ...@@ -397,28 +397,16 @@ module API
present merge_request, with: Entities::MergeRequest, current_user: current_user, project: user_project present merge_request, with: Entities::MergeRequest, current_user: current_user, project: user_project
end end
desc 'Merge a merge request to its default temporary merge ref path' desc 'Returns the up to date merge-ref HEAD commit'
params do get ':id/merge_requests/:merge_request_iid/merge_ref' do
optional :merge_commit_message, type: String, desc: 'Custom merge commit message'
end
put ':id/merge_requests/:merge_request_iid/merge_to_ref' do
merge_request = find_project_merge_request(params[:merge_request_iid]) merge_request = find_project_merge_request(params[:merge_request_iid])
authorize! :admin_merge_request, user_project result = ::MergeRequests::MergeabilityCheckService.new(merge_request).execute(recheck: true)
merge_params = {
commit_message: params[:merge_commit_message]
}
result = ::MergeRequests::MergeToRefService
.new(merge_request.target_project, current_user, merge_params)
.execute(merge_request)
if result[:status] == :success if result.success?
present result.slice(:commit_id), 200 present :commit_id, result.payload.dig(:merge_ref_head, :commit_id)
else else
http_status = result[:http_status] || 400 render_api_error!(result.message, 400)
render_api_error!(result[:message], http_status)
end end
end end
......
# 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
...@@ -1996,57 +1996,6 @@ describe MergeRequest do ...@@ -1996,57 +1996,6 @@ describe MergeRequest do
end end
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 describe '#mergeable?' do
let(:project) { create(:project) } let(:project) { create(:project) }
...@@ -2060,7 +2009,7 @@ describe MergeRequest do ...@@ -2060,7 +2009,7 @@ describe MergeRequest do
it 'return true if #mergeable_state? is true and the MR #can_be_merged? is true' 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 } 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).to receive(:can_be_merged?) { true }
expect(subject.mergeable?).to be_truthy expect(subject.mergeable?).to be_truthy
...@@ -2074,7 +2023,7 @@ describe MergeRequest do ...@@ -2074,7 +2023,7 @@ describe MergeRequest do
it 'checks if merge request can be merged' do it 'checks if merge request can be merged' do
allow(subject).to receive(:mergeable_ci_state?) { true } 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? subject.mergeable?
end end
...@@ -3142,38 +3091,6 @@ describe MergeRequest do ...@@ -3142,38 +3091,6 @@ describe MergeRequest do
end end
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 describe '#merge_participants' do
it 'contains author' do it 'contains author' do
expect(subject.merge_participants).to eq([subject.author]) expect(subject.merge_participants).to eq([subject.author])
......
...@@ -1546,52 +1546,80 @@ describe API::MergeRequests do ...@@ -1546,52 +1546,80 @@ describe API::MergeRequests do
end end
end end
describe "PUT /projects/:id/merge_requests/:merge_request_iid/merge_to_ref" do describe "GET /projects/:id/merge_requests/:merge_request_iid/merge_ref" do
let(:pipeline) { create(:ci_pipeline_without_jobs) } before do
merge_request.mark_as_unchecked!
end
let(:merge_request_iid) { merge_request.iid }
let(:url) do 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 end
it 'returns the generated ID from the merge service in case of success' do it 'returns the generated ID from the merge service in case of success' do
put api(url, user), params: { merge_commit_message: 'Custom message' } get api(url, user)
commit = project.commit(json_response['commit_id'])
expect(response).to have_gitlab_http_status(200) expect(response).to have_gitlab_http_status(200)
expect(json_response['commit_id']).to be_present expect(json_response['commit_id']).to eq(merge_request.merge_ref_head.sha)
expect(commit.message).to eq('Custom message')
end end
it "returns 400 if branch can't be merged" do context 'when merge-ref is not synced with merge status' do
merge_request.update!(state: 'merged') 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(response).to have_gitlab_http_status(200)
expect(json_response['message']) expect(json_response['commit_id']).to eq(merge_request.merge_ref_head.sha)
.to eq("Merge request is not mergeable to #{merge_request.merge_ref_path}") 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 end
it 'returns 403 if user has no permissions to merge to the ref' do context 'when user has no access to the MR' do
user2 = create(:user) let(:project) { create(:project, :private) }
project.add_reporter(user2) 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) get api(url, user)
expect(json_response['message']).to eq('403 Forbidden')
expect(response).to have_gitlab_http_status(404)
expect(json_response['message']).to eq('404 Not found')
end
end end
it 'returns 404 for an invalid merge request IID' do context 'when invalid merge request IID' do
put api("/projects/#{project.id}/merge_requests/12345/merge_to_ref", user) 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 end
it "returns 404 if the merge request id is used instead of iid" do context 'when merge request ID is used instead IID' do
put api("/projects/#{project.id}/merge_requests/#{merge_request.id}/merge", user) 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
end end
......
...@@ -72,10 +72,6 @@ describe MergeRequests::MergeToRefService do ...@@ -72,10 +72,6 @@ describe MergeRequests::MergeToRefService do
let(:merge_request) { create(:merge_request, :simple) } let(:merge_request) { create(:merge_request, :simple) }
let(:project) { merge_request.project } let(:project) { merge_request.project }
before do
project.add_maintainer(user)
end
describe '#execute' do describe '#execute' do
let(:service) do let(:service) do
described_class.new(project, user, commit_message: 'Awesome message', described_class.new(project, user, commit_message: 'Awesome message',
...@@ -92,6 +88,12 @@ describe MergeRequests::MergeToRefService do ...@@ -92,6 +88,12 @@ describe MergeRequests::MergeToRefService do
it_behaves_like 'successfully evaluates pre-condition checks' it_behaves_like 'successfully evaluates pre-condition checks'
context 'commit history comparison with regular MergeService' do 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 let(:merge_ref_service) do
described_class.new(project, user, {}) described_class.new(project, user, {})
end end
...@@ -104,12 +106,18 @@ describe MergeRequests::MergeToRefService do ...@@ -104,12 +106,18 @@ describe MergeRequests::MergeToRefService do
it_behaves_like 'MergeService for target ref' it_behaves_like 'MergeService for target ref'
end end
context 'when merge commit with squash', :quarantine do context 'when merge commit with squash' do
before do before do
merge_request.update!(squash: true, source_branch: 'master', target_branch: 'feature') merge_request.update!(squash: true)
end end
it_behaves_like 'MergeService for target ref' 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
end end
...@@ -136,9 +144,9 @@ describe MergeRequests::MergeToRefService do ...@@ -136,9 +144,9 @@ describe MergeRequests::MergeToRefService do
let(:merge_method) { :merge } let(:merge_method) { :merge }
it 'returns error' do 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) result = service.execute(merge_request)
...@@ -170,28 +178,5 @@ describe MergeRequests::MergeToRefService do ...@@ -170,28 +178,5 @@ describe MergeRequests::MergeToRefService do
it { expect(todo).not_to be_done } it { expect(todo).not_to be_done }
end 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
end end
# 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 'ignores merge-ref and updates 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 and merge-ref auto-sync is disabled' do
before do
stub_feature_flags(merge_ref_auto_sync: false)
merge_request.mark_as_mergeable!
end
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 ref is outdated due to disabled feature')
expect(result.payload).to be_empty
end
it 'merge status is not changed' do
subject
expect(merge_request.merge_status).to eq('can_be_merged')
end
end
context 'when MR is marked as mergeable, but repo is not mergeable and MR is not opened' do
before do
# Making sure that we don't touch the merge-status after
# the MR is not opened any longer. Source branch might
# have been removed, etc.
allow(merge_request).to receive(:broken?) { true }
merge_request.mark_as_mergeable!
merge_request.close!
end
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 ref cannot be updated')
expect(result.payload).to be_empty
end
it 'does not change the merge status' do
expect { subject }.not_to change(merge_request, :merge_status).from('can_be_merged')
end
end
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
context 'when MR is mergeable but merge-ref is already updated' do
before do
MergeRequests::MergeToRefService.new(project, merge_request.author).execute(merge_request)
merge_request.mark_as_mergeable!
end
it 'returns ServiceResponse.success' do
result = subject
expect(result).to be_a(ServiceResponse)
expect(result).to be_success
expect(result.payload[:merge_ref_head]).to be_present
end
it 'does not recreate the merge-ref' do
expect(MergeRequests::MergeToRefService).not_to receive(:new)
subject
end
end
end
end
end
...@@ -16,6 +16,13 @@ describe ServiceResponse do ...@@ -16,6 +16,13 @@ describe ServiceResponse do
expect(response).to be_success expect(response).to be_success
expect(response.message).to eq('Good orange') expect(response.message).to eq('Good orange')
end 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 end
describe '.error' do describe '.error' do
...@@ -33,6 +40,15 @@ describe ServiceResponse do ...@@ -33,6 +40,15 @@ describe ServiceResponse do
expect(response.message).to eq('Bad apple') expect(response.message).to eq('Bad apple')
expect(response.http_status).to eq(400) expect(response.http_status).to eq(400)
end 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 end
describe '#success?' do describe '#success?' do
......
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