3264 lines
104 KiB
Ruby
3264 lines
104 KiB
Ruby
# frozen_string_literal: true
|
|
|
|
require 'spec_helper'
|
|
|
|
describe MergeRequest do
|
|
include RepoHelpers
|
|
include ProjectForksHelper
|
|
include ReactiveCachingHelpers
|
|
|
|
using RSpec::Parameterized::TableSyntax
|
|
|
|
subject { create(:merge_request) }
|
|
|
|
describe 'associations' do
|
|
it { is_expected.to belong_to(:target_project).class_name('Project') }
|
|
it { is_expected.to belong_to(:source_project).class_name('Project') }
|
|
it { is_expected.to belong_to(:merge_user).class_name("User") }
|
|
it { is_expected.to have_many(:assignees).through(:merge_request_assignees) }
|
|
it { is_expected.to have_many(:merge_request_diffs) }
|
|
|
|
context 'for forks' do
|
|
let!(:project) { create(:project) }
|
|
let!(:fork) { fork_project(project) }
|
|
let!(:merge_request) { create(:merge_request, target_project: project, source_project: fork) }
|
|
|
|
it 'does not load another project due to inverse relationship' do
|
|
expect(project.merge_requests.first.target_project.object_id).to eq(project.object_id)
|
|
end
|
|
|
|
it 'finds the associated merge request' do
|
|
expect(project.merge_requests.find(merge_request.id)).to eq(merge_request)
|
|
end
|
|
end
|
|
end
|
|
|
|
describe 'locking' do
|
|
using RSpec::Parameterized::TableSyntax
|
|
|
|
where(:lock_version) do
|
|
[
|
|
[0],
|
|
["0"]
|
|
]
|
|
end
|
|
|
|
with_them do
|
|
it 'works when a merge request has a NULL lock_version' do
|
|
merge_request = create(:merge_request)
|
|
|
|
described_class.where(id: merge_request.id).update_all('lock_version = NULL')
|
|
|
|
merge_request.update!(lock_version: lock_version, title: 'locking test')
|
|
|
|
expect(merge_request.reload.title).to eq('locking test')
|
|
end
|
|
end
|
|
end
|
|
|
|
describe '#squash_in_progress?' do
|
|
let(:repo_path) do
|
|
Gitlab::GitalyClient::StorageSettings.allow_disk_access do
|
|
subject.source_project.repository.path
|
|
end
|
|
end
|
|
let(:squash_path) { File.join(repo_path, "gitlab-worktree", "squash-#{subject.id}") }
|
|
|
|
before do
|
|
system(*%W(#{Gitlab.config.git.bin_path} -C #{repo_path} worktree add --detach #{squash_path} master))
|
|
end
|
|
|
|
it 'returns true when there is a current squash directory' do
|
|
expect(subject.squash_in_progress?).to be_truthy
|
|
end
|
|
|
|
it 'returns false when there is no squash directory' do
|
|
FileUtils.rm_rf(squash_path)
|
|
|
|
expect(subject.squash_in_progress?).to be_falsey
|
|
end
|
|
|
|
it 'returns false when the squash directory has expired' do
|
|
time = 20.minutes.ago.to_time
|
|
File.utime(time, time, squash_path)
|
|
|
|
expect(subject.squash_in_progress?).to be_falsey
|
|
end
|
|
|
|
it 'returns false when the source project has been removed' do
|
|
allow(subject).to receive(:source_project).and_return(nil)
|
|
|
|
expect(subject.squash_in_progress?).to be_falsey
|
|
end
|
|
end
|
|
|
|
describe '#squash?' do
|
|
let(:merge_request) { build(:merge_request, squash: squash) }
|
|
subject { merge_request.squash? }
|
|
|
|
context 'disabled in database' do
|
|
let(:squash) { false }
|
|
|
|
it { is_expected.to be_falsy }
|
|
end
|
|
|
|
context 'enabled in database' do
|
|
let(:squash) { true }
|
|
|
|
it { is_expected.to be_truthy }
|
|
end
|
|
end
|
|
|
|
describe '#default_squash_commit_message' do
|
|
let(:project) { subject.project }
|
|
let(:is_multiline) { -> (c) { c.description.present? } }
|
|
let(:multiline_commits) { subject.commits.select(&is_multiline) }
|
|
let(:singleline_commits) { subject.commits.reject(&is_multiline) }
|
|
|
|
it 'returns the oldest multiline commit message' do
|
|
expect(subject.default_squash_commit_message).to eq(multiline_commits.last.message)
|
|
end
|
|
|
|
it 'returns the merge request title if there are no multiline commits' do
|
|
expect(subject).to receive(:commits).and_return(
|
|
CommitCollection.new(project, singleline_commits)
|
|
)
|
|
|
|
expect(subject.default_squash_commit_message).to eq(subject.title)
|
|
end
|
|
|
|
it 'does not return commit messages from multiline merge commits' do
|
|
collection = CommitCollection.new(project, multiline_commits).enrich!
|
|
|
|
expect(collection.commits).to all( receive(:merge_commit?).and_return(true) )
|
|
expect(subject).to receive(:commits).and_return(collection)
|
|
expect(subject.default_squash_commit_message).to eq(subject.title)
|
|
end
|
|
end
|
|
|
|
describe 'modules' do
|
|
subject { described_class }
|
|
|
|
it { is_expected.to include_module(Issuable) }
|
|
it { is_expected.to include_module(Referable) }
|
|
it { is_expected.to include_module(Sortable) }
|
|
it { is_expected.to include_module(Taskable) }
|
|
|
|
it_behaves_like 'AtomicInternalId' do
|
|
let(:internal_id_attribute) { :iid }
|
|
let(:instance) { build(:merge_request) }
|
|
let(:scope) { :target_project }
|
|
let(:scope_attrs) { { project: instance.target_project } }
|
|
let(:usage) { :merge_requests }
|
|
end
|
|
end
|
|
|
|
describe 'validation' do
|
|
it { is_expected.to validate_presence_of(:target_branch) }
|
|
it { is_expected.to validate_presence_of(:source_branch) }
|
|
|
|
context "Validation of merge user with Merge When Pipeline Succeeds" do
|
|
it "allows user to be nil when the feature is disabled" do
|
|
expect(subject).to be_valid
|
|
end
|
|
|
|
it "is invalid without merge user" do
|
|
subject.merge_when_pipeline_succeeds = true
|
|
expect(subject).not_to be_valid
|
|
end
|
|
|
|
it "is valid with merge user" do
|
|
subject.merge_when_pipeline_succeeds = true
|
|
subject.merge_user = build(:user)
|
|
|
|
expect(subject).to be_valid
|
|
end
|
|
end
|
|
|
|
context 'for branch' do
|
|
before do
|
|
stub_feature_flags(stricter_mr_branch_name: false)
|
|
end
|
|
|
|
using RSpec::Parameterized::TableSyntax
|
|
|
|
where(:branch_name, :valid) do
|
|
'foo' | true
|
|
'foo:bar' | false
|
|
'+foo:bar' | false
|
|
'foo bar' | false
|
|
'-foo' | false
|
|
'HEAD' | true
|
|
'refs/heads/master' | true
|
|
end
|
|
|
|
with_them do
|
|
it "validates source_branch" do
|
|
subject = build(:merge_request, source_branch: branch_name, target_branch: 'master')
|
|
|
|
subject.valid?
|
|
|
|
expect(subject.errors.added?(:source_branch)).to eq(!valid)
|
|
end
|
|
|
|
it "validates target_branch" do
|
|
subject = build(:merge_request, source_branch: 'master', target_branch: branch_name)
|
|
|
|
subject.valid?
|
|
|
|
expect(subject.errors.added?(:target_branch)).to eq(!valid)
|
|
end
|
|
end
|
|
end
|
|
|
|
context 'for forks' do
|
|
let(:project) { create(:project) }
|
|
let(:fork1) { fork_project(project) }
|
|
let(:fork2) { fork_project(project) }
|
|
|
|
it 'allows merge requests for sibling-forks' do
|
|
subject.source_project = fork1
|
|
subject.target_project = fork2
|
|
|
|
expect(subject).to be_valid
|
|
end
|
|
end
|
|
end
|
|
|
|
describe 'callbacks' do
|
|
describe '#ensure_merge_request_metrics' do
|
|
it 'creates metrics after saving' do
|
|
merge_request = create(:merge_request)
|
|
|
|
expect(merge_request.metrics).to be_persisted
|
|
expect(MergeRequest::Metrics.count).to eq(1)
|
|
end
|
|
|
|
it 'does not duplicate metrics for a merge request' do
|
|
merge_request = create(:merge_request)
|
|
|
|
merge_request.mark_as_merged!
|
|
|
|
expect(MergeRequest::Metrics.count).to eq(1)
|
|
end
|
|
end
|
|
end
|
|
|
|
describe 'respond to' do
|
|
it { is_expected.to respond_to(:unchecked?) }
|
|
it { is_expected.to respond_to(:can_be_merged?) }
|
|
it { is_expected.to respond_to(:cannot_be_merged?) }
|
|
it { is_expected.to respond_to(:merge_params) }
|
|
it { is_expected.to respond_to(:merge_when_pipeline_succeeds) }
|
|
end
|
|
|
|
describe '.by_commit_sha' do
|
|
subject(:by_commit_sha) { described_class.by_commit_sha(sha) }
|
|
|
|
let!(:merge_request) { create(:merge_request, :with_diffs) }
|
|
|
|
context 'with sha contained in latest merge request diff' do
|
|
let(:sha) { 'b83d6e391c22777fca1ed3012fce84f633d7fed0' }
|
|
|
|
it 'returns merge requests' do
|
|
expect(by_commit_sha).to eq([merge_request])
|
|
end
|
|
end
|
|
|
|
context 'with sha contained not in latest merge request diff' do
|
|
let(:sha) { 'b83d6e391c22777fca1ed3012fce84f633d7fed0' }
|
|
|
|
it 'returns empty requests' do
|
|
latest_merge_request_diff = merge_request.merge_request_diffs.create
|
|
latest_merge_request_diff.merge_request_diff_commits.where(sha: 'b83d6e391c22777fca1ed3012fce84f633d7fed0').delete_all
|
|
|
|
expect(by_commit_sha).to be_empty
|
|
end
|
|
end
|
|
|
|
context 'with sha not contained in' do
|
|
let(:sha) { 'b83d6e3' }
|
|
|
|
it 'returns empty result' do
|
|
expect(by_commit_sha).to be_empty
|
|
end
|
|
end
|
|
end
|
|
|
|
describe '.in_projects' do
|
|
it 'returns the merge requests for a set of projects' do
|
|
expect(described_class.in_projects(Project.all)).to eq([subject])
|
|
end
|
|
end
|
|
|
|
describe '.set_latest_merge_request_diff_ids!' do
|
|
def create_merge_request_with_diffs(source_branch, diffs: 2)
|
|
params = {
|
|
target_project: project,
|
|
target_branch: 'master',
|
|
source_project: project,
|
|
source_branch: source_branch
|
|
}
|
|
|
|
create(:merge_request, params).tap do |mr|
|
|
diffs.times { mr.merge_request_diffs.create }
|
|
end
|
|
end
|
|
|
|
let(:project) { create(:project) }
|
|
|
|
it 'sets IDs for merge requests, whether they are already set or not' do
|
|
merge_requests = [
|
|
create_merge_request_with_diffs('feature'),
|
|
create_merge_request_with_diffs('feature-conflict'),
|
|
create_merge_request_with_diffs('wip', diffs: 0),
|
|
create_merge_request_with_diffs('csv')
|
|
]
|
|
|
|
merge_requests.take(2).each do |merge_request|
|
|
merge_request.update_column(:latest_merge_request_diff_id, nil)
|
|
end
|
|
|
|
expected = merge_requests.map do |merge_request|
|
|
merge_request.merge_request_diffs.maximum(:id)
|
|
end
|
|
|
|
expect { project.merge_requests.set_latest_merge_request_diff_ids! }
|
|
.to change { merge_requests.map { |mr| mr.reload.latest_merge_request_diff_id } }.to(expected)
|
|
end
|
|
end
|
|
|
|
describe '.recent_target_branches' do
|
|
let(:project) { create(:project) }
|
|
let!(:merge_request1) { create(:merge_request, :opened, source_project: project, target_branch: 'feature') }
|
|
let!(:merge_request2) { create(:merge_request, :closed, source_project: project, target_branch: 'merge-test') }
|
|
let!(:merge_request3) { create(:merge_request, :opened, source_project: project, target_branch: 'fix') }
|
|
let!(:merge_request4) { create(:merge_request, :closed, source_project: project, target_branch: 'feature') }
|
|
|
|
before do
|
|
merge_request1.update_columns(updated_at: 1.day.since)
|
|
merge_request2.update_columns(updated_at: 2.days.since)
|
|
merge_request3.update_columns(updated_at: 3.days.since)
|
|
merge_request4.update_columns(updated_at: 4.days.since)
|
|
end
|
|
|
|
it 'returns target branches sort by updated at desc' do
|
|
expect(described_class.recent_target_branches).to match_array(['feature', 'merge-test', 'fix'])
|
|
end
|
|
end
|
|
|
|
describe '#target_branch_sha' do
|
|
let(:project) { create(:project, :repository) }
|
|
|
|
subject { create(:merge_request, source_project: project, target_project: project) }
|
|
|
|
context 'when the target branch does not exist' do
|
|
before do
|
|
project.repository.rm_branch(subject.author, subject.target_branch)
|
|
subject.clear_memoized_shas
|
|
end
|
|
|
|
it 'returns nil' do
|
|
expect(subject.target_branch_sha).to be_nil
|
|
end
|
|
end
|
|
|
|
it 'returns memoized value' do
|
|
subject.target_branch_sha = '8ffb3c15a5475e59ae909384297fede4badcb4c7'
|
|
|
|
expect(subject.target_branch_sha).to eq '8ffb3c15a5475e59ae909384297fede4badcb4c7'
|
|
end
|
|
end
|
|
|
|
describe '#card_attributes' do
|
|
it 'includes the author name' do
|
|
allow(subject).to receive(:author).and_return(double(name: 'Robert'))
|
|
allow(subject).to receive(:assignees).and_return([])
|
|
|
|
expect(subject.card_attributes)
|
|
.to eq({ 'Author' => 'Robert', 'Assignee' => "" })
|
|
end
|
|
|
|
it 'includes the assignees name' do
|
|
allow(subject).to receive(:author).and_return(double(name: 'Robert'))
|
|
allow(subject).to receive(:assignees).and_return([double(name: 'Douwe'), double(name: 'Robert')])
|
|
|
|
expect(subject.card_attributes)
|
|
.to eq({ 'Author' => 'Robert', 'Assignee' => 'Douwe and Robert' })
|
|
end
|
|
end
|
|
|
|
describe '#assignee_or_author?' do
|
|
let(:user) { create(:user) }
|
|
|
|
it 'returns true for a user that is assigned to a merge request' do
|
|
subject.assignees = [user]
|
|
|
|
expect(subject.assignee_or_author?(user)).to eq(true)
|
|
end
|
|
|
|
it 'returns true for a user that is the author of a merge request' do
|
|
subject.author = user
|
|
|
|
expect(subject.assignee_or_author?(user)).to eq(true)
|
|
end
|
|
|
|
it 'returns false for a user that is not the assignee or author' do
|
|
expect(subject.assignee_or_author?(user)).to eq(false)
|
|
end
|
|
end
|
|
|
|
describe '#visible_closing_issues_for' do
|
|
let(:guest) { create(:user) }
|
|
let(:developer) { create(:user) }
|
|
let(:issue_1) { create(:issue, project: subject.source_project) }
|
|
let(:issue_2) { create(:issue, project: subject.source_project) }
|
|
let(:confidential_issue) { create(:issue, :confidential, project: subject.source_project) }
|
|
|
|
before do
|
|
subject.project.add_developer(subject.author)
|
|
subject.target_branch = subject.project.default_branch
|
|
commit = double('commit1', safe_message: "Fixes #{issue_1.to_reference} #{issue_2.to_reference} #{confidential_issue.to_reference}")
|
|
allow(subject).to receive(:commits).and_return([commit])
|
|
end
|
|
|
|
it 'shows only allowed issues to guest' do
|
|
subject.project.add_guest(guest)
|
|
|
|
subject.cache_merge_request_closes_issues!
|
|
|
|
expect(subject.visible_closing_issues_for(guest)).to match_array([issue_1, issue_2])
|
|
end
|
|
|
|
it 'shows only allowed issues to developer' do
|
|
subject.project.add_developer(developer)
|
|
|
|
subject.cache_merge_request_closes_issues!
|
|
|
|
expect(subject.visible_closing_issues_for(developer)).to match_array([issue_1, confidential_issue, issue_2])
|
|
end
|
|
|
|
context 'when external issue tracker is enabled' do
|
|
before do
|
|
subject.project.has_external_issue_tracker = true
|
|
subject.project.save!
|
|
end
|
|
|
|
it 'calls non #closes_issues to retrieve data' do
|
|
expect(subject).to receive(:closes_issues)
|
|
expect(subject).not_to receive(:cached_closes_issues)
|
|
|
|
subject.visible_closing_issues_for
|
|
end
|
|
end
|
|
end
|
|
|
|
describe '#cache_merge_request_closes_issues!' do
|
|
before do
|
|
subject.project.add_developer(subject.author)
|
|
subject.target_branch = subject.project.default_branch
|
|
end
|
|
|
|
it 'caches closed issues' do
|
|
issue = create :issue, project: subject.project
|
|
commit = double('commit1', safe_message: "Fixes #{issue.to_reference}")
|
|
allow(subject).to receive(:commits).and_return([commit])
|
|
|
|
expect { subject.cache_merge_request_closes_issues!(subject.author) }.to change(subject.merge_requests_closing_issues, :count).by(1)
|
|
end
|
|
|
|
it 'does not cache closed issues when merge request is closed' do
|
|
issue = create :issue, project: subject.project
|
|
commit = double('commit1', safe_message: "Fixes #{issue.to_reference}")
|
|
|
|
allow(subject).to receive(:commits).and_return([commit])
|
|
allow(subject).to receive(:state).and_return("closed")
|
|
|
|
expect { subject.cache_merge_request_closes_issues!(subject.author) }.not_to change(subject.merge_requests_closing_issues, :count)
|
|
end
|
|
|
|
it 'does not cache closed issues when merge request is merged' do
|
|
issue = create :issue, project: subject.project
|
|
commit = double('commit1', safe_message: "Fixes #{issue.to_reference}")
|
|
allow(subject).to receive(:commits).and_return([commit])
|
|
allow(subject).to receive(:state).and_return("merged")
|
|
|
|
expect { subject.cache_merge_request_closes_issues!(subject.author) }.not_to change(subject.merge_requests_closing_issues, :count)
|
|
end
|
|
|
|
context 'when both internal and external issue trackers are enabled' do
|
|
before do
|
|
subject.project.has_external_issue_tracker = true
|
|
subject.project.save!
|
|
create(:jira_service, project: subject.project)
|
|
end
|
|
|
|
it 'does not cache issues from external trackers' do
|
|
issue = ExternalIssue.new('JIRA-123', subject.project)
|
|
commit = double('commit1', safe_message: "Fixes #{issue.to_reference}")
|
|
allow(subject).to receive(:commits).and_return([commit])
|
|
|
|
expect { subject.cache_merge_request_closes_issues!(subject.author) }.not_to raise_error
|
|
expect { subject.cache_merge_request_closes_issues!(subject.author) }.not_to change(subject.merge_requests_closing_issues, :count)
|
|
end
|
|
|
|
it 'caches an internal issue' do
|
|
issue = create(:issue, project: subject.project)
|
|
commit = double('commit1', safe_message: "Fixes #{issue.to_reference}")
|
|
allow(subject).to receive(:commits).and_return([commit])
|
|
|
|
expect { subject.cache_merge_request_closes_issues!(subject.author) }
|
|
.to change(subject.merge_requests_closing_issues, :count).by(1)
|
|
end
|
|
end
|
|
|
|
context 'when only external issue tracker enabled' do
|
|
before do
|
|
subject.project.has_external_issue_tracker = true
|
|
subject.project.issues_enabled = false
|
|
subject.project.save!
|
|
end
|
|
|
|
it 'does not cache issues from external trackers' do
|
|
issue = ExternalIssue.new('JIRA-123', subject.project)
|
|
commit = double('commit1', safe_message: "Fixes #{issue.to_reference}")
|
|
allow(subject).to receive(:commits).and_return([commit])
|
|
|
|
expect { subject.cache_merge_request_closes_issues!(subject.author) }.not_to change(subject.merge_requests_closing_issues, :count)
|
|
end
|
|
|
|
it 'does not cache an internal issue' do
|
|
issue = create(:issue, project: subject.project)
|
|
commit = double('commit1', safe_message: "Fixes #{issue.to_reference}")
|
|
allow(subject).to receive(:commits).and_return([commit])
|
|
|
|
expect { subject.cache_merge_request_closes_issues!(subject.author) }
|
|
.not_to change(subject.merge_requests_closing_issues, :count)
|
|
end
|
|
end
|
|
end
|
|
|
|
describe '#source_branch_sha' do
|
|
let(:last_branch_commit) { subject.source_project.repository.commit(Gitlab::Git::BRANCH_REF_PREFIX + subject.source_branch) }
|
|
|
|
context 'with diffs' do
|
|
subject { create(:merge_request, :with_diffs) }
|
|
it 'returns the sha of the source branch last commit' do
|
|
expect(subject.source_branch_sha).to eq(last_branch_commit.sha)
|
|
end
|
|
end
|
|
|
|
context 'without diffs' do
|
|
subject { create(:merge_request, :without_diffs) }
|
|
it 'returns the sha of the source branch last commit' do
|
|
expect(subject.source_branch_sha).to eq(last_branch_commit.sha)
|
|
end
|
|
|
|
context 'when there is a tag name matching the branch name' do
|
|
let(:tag_name) { subject.source_branch }
|
|
|
|
it 'returns the sha of the source branch last commit' do
|
|
subject.source_project.repository.add_tag(subject.author,
|
|
tag_name,
|
|
subject.target_branch_sha,
|
|
'Add a tag')
|
|
|
|
expect(subject.source_branch_sha).to eq(last_branch_commit.sha)
|
|
|
|
subject.source_project.repository.rm_tag(subject.author, tag_name)
|
|
end
|
|
end
|
|
end
|
|
|
|
context 'when the merge request is being created' do
|
|
subject { build(:merge_request, source_branch: nil, compare_commits: []) }
|
|
it 'returns nil' do
|
|
expect(subject.source_branch_sha).to be_nil
|
|
end
|
|
end
|
|
|
|
it 'returns memoized value' do
|
|
subject.source_branch_sha = '2e5d3239642f9161dcbbc4b70a211a68e5e45e2b'
|
|
|
|
expect(subject.source_branch_sha).to eq '2e5d3239642f9161dcbbc4b70a211a68e5e45e2b'
|
|
end
|
|
end
|
|
|
|
describe '#to_reference' do
|
|
let(:project) { build(:project, name: 'sample-project') }
|
|
let(:merge_request) { build(:merge_request, target_project: project, iid: 1) }
|
|
|
|
it 'returns a String reference to the object' do
|
|
expect(merge_request.to_reference).to eq "!1"
|
|
end
|
|
|
|
it 'supports a cross-project reference' do
|
|
another_project = build(:project, name: 'another-project', namespace: project.namespace)
|
|
expect(merge_request.to_reference(another_project)).to eq "sample-project!1"
|
|
end
|
|
|
|
it 'returns a String reference with the full path' do
|
|
expect(merge_request.to_reference(full: true)).to eq(project.full_path + '!1')
|
|
end
|
|
end
|
|
|
|
describe '#raw_diffs' do
|
|
let(:merge_request) { build(:merge_request) }
|
|
let(:options) { { paths: ['a/b', 'b/a', 'c/*'] } }
|
|
|
|
context 'when there are MR diffs' do
|
|
it 'delegates to the MR diffs' do
|
|
merge_request.merge_request_diff = MergeRequestDiff.new
|
|
|
|
expect(merge_request.merge_request_diff).to receive(:raw_diffs).with(options)
|
|
|
|
merge_request.raw_diffs(options)
|
|
end
|
|
end
|
|
|
|
context 'when there are no MR diffs' do
|
|
it 'delegates to the compare object' do
|
|
merge_request.compare = double(:compare)
|
|
|
|
expect(merge_request.compare).to receive(:raw_diffs).with(options)
|
|
|
|
merge_request.raw_diffs(options)
|
|
end
|
|
end
|
|
end
|
|
|
|
describe '#diffs' do
|
|
let(:merge_request) { build(:merge_request) }
|
|
let(:options) { { paths: ['a/b', 'b/a', 'c/*'] } }
|
|
|
|
context 'when there are MR diffs' do
|
|
it 'delegates to the MR diffs' do
|
|
merge_request.save
|
|
|
|
expect(merge_request.merge_request_diff).to receive(:raw_diffs).with(hash_including(options)).and_call_original
|
|
|
|
merge_request.diffs(options).diff_files
|
|
end
|
|
end
|
|
|
|
context 'when there are no MR diffs' do
|
|
it 'delegates to the compare object, setting expanded: true' do
|
|
merge_request.compare = double(:compare)
|
|
|
|
expect(merge_request.compare).to receive(:diffs).with(options.merge(expanded: true))
|
|
|
|
merge_request.diffs(options)
|
|
end
|
|
end
|
|
end
|
|
|
|
describe '#preload_discussions_diff_highlight' do
|
|
let(:merge_request) { create(:merge_request) }
|
|
|
|
context 'with commit diff note' do
|
|
let(:other_merge_request) { create(:merge_request) }
|
|
|
|
let!(:diff_note) do
|
|
create(:diff_note_on_commit, project: merge_request.project)
|
|
end
|
|
|
|
let!(:other_mr_diff_note) do
|
|
create(:diff_note_on_commit, project: other_merge_request.project)
|
|
end
|
|
|
|
it 'preloads diff highlighting' do
|
|
expect_next_instance_of(Gitlab::DiscussionsDiff::FileCollection) do |collection|
|
|
note_diff_file = diff_note.note_diff_file
|
|
|
|
expect(collection)
|
|
.to receive(:load_highlight)
|
|
.with([note_diff_file.id]).and_call_original
|
|
end
|
|
|
|
merge_request.preload_discussions_diff_highlight
|
|
end
|
|
end
|
|
|
|
context 'with merge request diff note' do
|
|
let!(:unresolved_diff_note) do
|
|
create(:diff_note_on_merge_request, project: merge_request.project, noteable: merge_request)
|
|
end
|
|
|
|
let!(:resolved_diff_note) do
|
|
create(:diff_note_on_merge_request, :resolved, project: merge_request.project, noteable: merge_request)
|
|
end
|
|
|
|
it 'preloads diff highlighting' do
|
|
expect_next_instance_of(Gitlab::DiscussionsDiff::FileCollection) do |collection|
|
|
note_diff_file = unresolved_diff_note.note_diff_file
|
|
|
|
expect(collection)
|
|
.to receive(:load_highlight)
|
|
.with([note_diff_file.id])
|
|
.and_call_original
|
|
end
|
|
|
|
merge_request.preload_discussions_diff_highlight
|
|
end
|
|
end
|
|
end
|
|
|
|
describe '#diff_size' do
|
|
let(:merge_request) do
|
|
build(:merge_request, source_branch: 'expand-collapse-files', target_branch: 'master')
|
|
end
|
|
|
|
context 'when there are MR diffs' do
|
|
it 'returns the correct count' do
|
|
merge_request.save
|
|
|
|
expect(merge_request.diff_size).to eq('105')
|
|
end
|
|
|
|
it 'returns the correct overflow count' do
|
|
allow(Commit).to receive(:max_diff_options).and_return(max_files: 2)
|
|
merge_request.save
|
|
|
|
expect(merge_request.diff_size).to eq('2+')
|
|
end
|
|
|
|
it 'does not perform highlighting' do
|
|
merge_request.save
|
|
|
|
expect(Gitlab::Diff::Highlight).not_to receive(:new)
|
|
|
|
merge_request.diff_size
|
|
end
|
|
end
|
|
|
|
context 'when there are no MR diffs' do
|
|
def set_compare(merge_request)
|
|
merge_request.compare = CompareService.new(
|
|
merge_request.source_project,
|
|
merge_request.source_branch
|
|
).execute(
|
|
merge_request.target_project,
|
|
merge_request.target_branch
|
|
)
|
|
end
|
|
|
|
it 'returns the correct count' do
|
|
set_compare(merge_request)
|
|
|
|
expect(merge_request.diff_size).to eq('105')
|
|
end
|
|
|
|
it 'returns the correct overflow count' do
|
|
allow(Commit).to receive(:max_diff_options).and_return(max_files: 2)
|
|
set_compare(merge_request)
|
|
|
|
expect(merge_request.diff_size).to eq('2+')
|
|
end
|
|
|
|
it 'does not perform highlighting' do
|
|
set_compare(merge_request)
|
|
|
|
expect(Gitlab::Diff::Highlight).not_to receive(:new)
|
|
|
|
merge_request.diff_size
|
|
end
|
|
end
|
|
end
|
|
|
|
describe '#modified_paths' do
|
|
let(:paths) { double(:paths) }
|
|
subject(:merge_request) { build(:merge_request) }
|
|
|
|
before do
|
|
expect(diff).to receive(:modified_paths).and_return(paths)
|
|
end
|
|
|
|
context 'when past_merge_request_diff is specified' do
|
|
let(:another_diff) { double(:merge_request_diff) }
|
|
let(:diff) { another_diff }
|
|
|
|
it 'returns affected file paths from specified past_merge_request_diff' do
|
|
expect(merge_request.modified_paths(past_merge_request_diff: another_diff)).to eq(paths)
|
|
end
|
|
end
|
|
|
|
context 'when compare is present' do
|
|
let(:compare) { double(:compare) }
|
|
let(:diff) { compare }
|
|
|
|
it 'returns affected file paths from compare' do
|
|
merge_request.compare = compare
|
|
|
|
expect(merge_request.modified_paths).to eq(paths)
|
|
end
|
|
end
|
|
|
|
context 'when no arguments provided' do
|
|
let(:diff) { merge_request.merge_request_diff }
|
|
subject(:merge_request) { create(:merge_request, source_branch: 'feature', target_branch: 'master') }
|
|
|
|
it 'returns affected file paths for merge_request_diff' do
|
|
expect(merge_request.modified_paths).to eq(paths)
|
|
end
|
|
end
|
|
end
|
|
|
|
describe "#related_notes" do
|
|
let!(:merge_request) { create(:merge_request) }
|
|
|
|
before do
|
|
allow(merge_request).to receive(:commits) { [merge_request.source_project.repository.commit] }
|
|
create(:note_on_commit, commit_id: merge_request.commits.first.id,
|
|
project: merge_request.project)
|
|
create(:note, noteable: merge_request, project: merge_request.project)
|
|
end
|
|
|
|
it "includes notes for commits" do
|
|
expect(merge_request.commits).not_to be_empty
|
|
expect(merge_request.related_notes.count).to eq(2)
|
|
end
|
|
|
|
it "includes notes for commits from target project as well" do
|
|
create(:note_on_commit, commit_id: merge_request.commits.first.id,
|
|
project: merge_request.target_project)
|
|
|
|
expect(merge_request.commits).not_to be_empty
|
|
expect(merge_request.related_notes.count).to eq(3)
|
|
end
|
|
|
|
it "excludes system notes for commits" do
|
|
system_note = create(:note_on_commit, :system, commit_id: merge_request.commits.first.id,
|
|
project: merge_request.project)
|
|
|
|
expect(merge_request.related_notes.count).to eq(2)
|
|
expect(merge_request.related_notes).not_to include(system_note)
|
|
end
|
|
end
|
|
|
|
describe '#for_fork?' do
|
|
it 'returns true if the merge request is for a fork' do
|
|
subject.source_project = build_stubbed(:project, namespace: create(:group))
|
|
subject.target_project = build_stubbed(:project, namespace: create(:group))
|
|
|
|
expect(subject.for_fork?).to be_truthy
|
|
end
|
|
|
|
it 'returns false if is not for a fork' do
|
|
expect(subject.for_fork?).to be_falsey
|
|
end
|
|
end
|
|
|
|
describe '#closes_issues' do
|
|
let(:issue0) { create :issue, project: subject.project }
|
|
let(:issue1) { create :issue, project: subject.project }
|
|
|
|
let(:commit0) { double('commit0', safe_message: "Fixes #{issue0.to_reference}") }
|
|
let(:commit1) { double('commit1', safe_message: "Fixes #{issue0.to_reference}") }
|
|
let(:commit2) { double('commit2', safe_message: "Fixes #{issue1.to_reference}") }
|
|
|
|
before do
|
|
subject.project.add_developer(subject.author)
|
|
allow(subject).to receive(:commits).and_return([commit0, commit1, commit2])
|
|
end
|
|
|
|
it 'accesses the set of issues that will be closed on acceptance' do
|
|
allow(subject.project).to receive(:default_branch)
|
|
.and_return(subject.target_branch)
|
|
|
|
closed = subject.closes_issues
|
|
|
|
expect(closed).to include(issue0, issue1)
|
|
end
|
|
|
|
it 'only lists issues as to be closed if it targets the default branch' do
|
|
allow(subject.project).to receive(:default_branch).and_return('master')
|
|
subject.target_branch = 'something-else'
|
|
|
|
expect(subject.closes_issues).to be_empty
|
|
end
|
|
end
|
|
|
|
describe '#issues_mentioned_but_not_closing' do
|
|
let(:closing_issue) { create :issue, project: subject.project }
|
|
let(:mentioned_issue) { create :issue, project: subject.project }
|
|
|
|
let(:commit) { double('commit', safe_message: "Fixes #{closing_issue.to_reference}") }
|
|
|
|
it 'detects issues mentioned in description but not closed' do
|
|
subject.project.add_developer(subject.author)
|
|
subject.description = "Is related to #{mentioned_issue.to_reference} and #{closing_issue.to_reference}"
|
|
|
|
allow(subject).to receive(:commits).and_return([commit])
|
|
allow(subject.project).to receive(:default_branch)
|
|
.and_return(subject.target_branch)
|
|
subject.cache_merge_request_closes_issues!
|
|
|
|
expect(subject.issues_mentioned_but_not_closing(subject.author)).to match_array([mentioned_issue])
|
|
end
|
|
|
|
context 'when the project has an external issue tracker' do
|
|
before do
|
|
subject.project.add_developer(subject.author)
|
|
commit = double(:commit, safe_message: 'Fixes TEST-3')
|
|
|
|
create(:jira_service, project: subject.project)
|
|
|
|
allow(subject).to receive(:commits).and_return([commit])
|
|
allow(subject).to receive(:description).and_return('Is related to TEST-2 and TEST-3')
|
|
allow(subject.project).to receive(:default_branch).and_return(subject.target_branch)
|
|
end
|
|
|
|
it 'detects issues mentioned in description but not closed' do
|
|
subject.cache_merge_request_closes_issues!
|
|
|
|
expect(subject.issues_mentioned_but_not_closing(subject.author).map(&:to_s)).to match_array(['TEST-2'])
|
|
end
|
|
end
|
|
end
|
|
|
|
describe "#work_in_progress?" do
|
|
['WIP ', 'WIP:', 'WIP: ', '[WIP]', '[WIP] ', ' [WIP] WIP [WIP] WIP: WIP '].each do |wip_prefix|
|
|
it "detects the '#{wip_prefix}' prefix" do
|
|
subject.title = "#{wip_prefix}#{subject.title}"
|
|
expect(subject.work_in_progress?).to eq true
|
|
end
|
|
end
|
|
|
|
it "doesn't detect WIP for words starting with WIP" do
|
|
subject.title = "Wipwap #{subject.title}"
|
|
expect(subject.work_in_progress?).to eq false
|
|
end
|
|
|
|
it "doesn't detect WIP for words containing with WIP" do
|
|
subject.title = "WupWipwap #{subject.title}"
|
|
expect(subject.work_in_progress?).to eq false
|
|
end
|
|
|
|
it "doesn't detect WIP by default" do
|
|
expect(subject.work_in_progress?).to eq false
|
|
end
|
|
end
|
|
|
|
describe "#wipless_title" do
|
|
['WIP ', 'WIP:', 'WIP: ', '[WIP]', '[WIP] ', '[WIP] WIP [WIP] WIP: WIP '].each do |wip_prefix|
|
|
it "removes the '#{wip_prefix}' prefix" do
|
|
wipless_title = subject.title
|
|
subject.title = "#{wip_prefix}#{subject.title}"
|
|
|
|
expect(subject.wipless_title).to eq wipless_title
|
|
end
|
|
|
|
it "is satisfies the #work_in_progress? method" do
|
|
subject.title = "#{wip_prefix}#{subject.title}"
|
|
subject.title = subject.wipless_title
|
|
|
|
expect(subject.work_in_progress?).to eq false
|
|
end
|
|
end
|
|
end
|
|
|
|
describe "#wip_title" do
|
|
it "adds the WIP: prefix to the title" do
|
|
wip_title = "WIP: #{subject.title}"
|
|
|
|
expect(subject.wip_title).to eq wip_title
|
|
end
|
|
|
|
it "does not add the WIP: prefix multiple times" do
|
|
wip_title = "WIP: #{subject.title}"
|
|
subject.title = subject.wip_title
|
|
subject.title = subject.wip_title
|
|
|
|
expect(subject.wip_title).to eq wip_title
|
|
end
|
|
|
|
it "is satisfies the #work_in_progress? method" do
|
|
subject.title = subject.wip_title
|
|
|
|
expect(subject.work_in_progress?).to eq true
|
|
end
|
|
end
|
|
|
|
describe '#can_remove_source_branch?' do
|
|
set(:user) { create(:user) }
|
|
set(:merge_request) { create(:merge_request, :simple) }
|
|
|
|
subject { merge_request }
|
|
|
|
before do
|
|
subject.source_project.add_maintainer(user)
|
|
end
|
|
|
|
it "can't be removed when its a protected branch" do
|
|
allow(ProtectedBranch).to receive(:protected?).and_return(true)
|
|
|
|
expect(subject.can_remove_source_branch?(user)).to be_falsey
|
|
end
|
|
|
|
it "can't remove a root ref" do
|
|
subject.update(source_branch: 'master', target_branch: 'feature')
|
|
|
|
expect(subject.can_remove_source_branch?(user)).to be_falsey
|
|
end
|
|
|
|
it "is unable to remove the source branch for a project the user cannot push to" do
|
|
user2 = create(:user)
|
|
|
|
expect(subject.can_remove_source_branch?(user2)).to be_falsey
|
|
end
|
|
|
|
it "can be removed if the last commit is the head of the source branch" do
|
|
allow(subject).to receive(:source_branch_head).and_return(subject.diff_head_commit)
|
|
|
|
expect(subject.can_remove_source_branch?(user)).to be_truthy
|
|
end
|
|
|
|
it "cannot be removed if the last commit is not also the head of the source branch" do
|
|
subject.clear_memoized_shas
|
|
subject.source_branch = "lfs"
|
|
|
|
expect(subject.can_remove_source_branch?(user)).to be_falsey
|
|
end
|
|
end
|
|
|
|
describe '#default_merge_commit_message' do
|
|
it 'includes merge information as the title' do
|
|
request = build(:merge_request, source_branch: 'source', target_branch: 'target')
|
|
|
|
expect(request.default_merge_commit_message)
|
|
.to match("Merge branch 'source' into 'target'\n\n")
|
|
end
|
|
|
|
it 'includes its title in the body' do
|
|
request = build(:merge_request, title: 'Remove all technical debt')
|
|
|
|
expect(request.default_merge_commit_message)
|
|
.to match("Remove all technical debt\n\n")
|
|
end
|
|
|
|
it 'includes its closed issues in the body' do
|
|
issue = create(:issue, project: subject.project)
|
|
|
|
subject.project.add_developer(subject.author)
|
|
subject.description = "This issue Closes #{issue.to_reference}"
|
|
allow(subject.project).to receive(:default_branch).and_return(subject.target_branch)
|
|
subject.cache_merge_request_closes_issues!
|
|
|
|
expect(subject.default_merge_commit_message)
|
|
.to match("Closes #{issue.to_reference}")
|
|
end
|
|
|
|
it 'includes its reference in the body' do
|
|
request = build_stubbed(:merge_request)
|
|
|
|
expect(request.default_merge_commit_message)
|
|
.to match("See merge request #{request.to_reference(full: true)}")
|
|
end
|
|
|
|
it 'excludes multiple linebreak runs when description is blank' do
|
|
request = build(:merge_request, title: 'Title', description: nil)
|
|
|
|
expect(request.default_merge_commit_message).not_to match("Title\n\n\n\n")
|
|
end
|
|
|
|
it 'includes its description in the body' do
|
|
request = build(:merge_request, description: 'By removing all code')
|
|
|
|
expect(request.default_merge_commit_message(include_description: true))
|
|
.to match("By removing all code\n\n")
|
|
end
|
|
|
|
it 'does not includes its description in the body' do
|
|
request = build(:merge_request, description: 'By removing all code')
|
|
|
|
expect(request.default_merge_commit_message)
|
|
.not_to match("By removing all code\n\n")
|
|
end
|
|
end
|
|
|
|
describe "#auto_merge_strategy" do
|
|
subject { merge_request.auto_merge_strategy }
|
|
|
|
let(:merge_request) { create(:merge_request, :merge_when_pipeline_succeeds) }
|
|
|
|
it { is_expected.to eq('merge_when_pipeline_succeeds') }
|
|
|
|
context 'when auto merge is disabled' do
|
|
let(:merge_request) { create(:merge_request) }
|
|
|
|
it { is_expected.to be_nil }
|
|
end
|
|
end
|
|
|
|
describe '#committers' do
|
|
it 'returns all the committers of every commit in the merge request' do
|
|
users = subject.commits.without_merge_commits.map(&:committer_email).uniq.map do |email|
|
|
create(:user, email: email)
|
|
end
|
|
|
|
expect(subject.committers).to match_array(users)
|
|
end
|
|
|
|
it 'returns an empty array if no committer is associated with a user' do
|
|
expect(subject.committers).to be_empty
|
|
end
|
|
end
|
|
|
|
describe '#hook_attrs' do
|
|
it 'delegates to Gitlab::HookData::MergeRequestBuilder#build' do
|
|
builder = double
|
|
|
|
expect(Gitlab::HookData::MergeRequestBuilder)
|
|
.to receive(:new).with(subject).and_return(builder)
|
|
expect(builder).to receive(:build)
|
|
|
|
subject.hook_attrs
|
|
end
|
|
end
|
|
|
|
describe '#diverged_commits_count' do
|
|
let(:project) { create(:project, :repository) }
|
|
let(:forked_project) { fork_project(project, nil, repository: true) }
|
|
|
|
context 'when the target branch does not exist anymore' do
|
|
subject { create(:merge_request, source_project: project, target_project: project) }
|
|
|
|
before do
|
|
project.repository.raw_repository.delete_branch(subject.target_branch)
|
|
subject.clear_memoized_shas
|
|
end
|
|
|
|
it 'does not crash' do
|
|
expect { subject.diverged_commits_count }.not_to raise_error
|
|
end
|
|
|
|
it 'returns 0' do
|
|
expect(subject.diverged_commits_count).to eq(0)
|
|
end
|
|
end
|
|
|
|
context 'diverged on same repository' do
|
|
subject(:merge_request_with_divergence) { create(:merge_request, :diverged, source_project: project, target_project: project) }
|
|
|
|
it 'counts commits that are on target branch but not on source branch' do
|
|
expect(subject.diverged_commits_count).to eq(29)
|
|
end
|
|
end
|
|
|
|
context 'diverged on fork' do
|
|
subject(:merge_request_fork_with_divergence) { create(:merge_request, :diverged, source_project: forked_project, target_project: project) }
|
|
|
|
it 'counts commits that are on target branch but not on source branch' do
|
|
expect(subject.diverged_commits_count).to eq(29)
|
|
end
|
|
end
|
|
|
|
context 'rebased on fork' do
|
|
subject(:merge_request_rebased) { create(:merge_request, :rebased, source_project: forked_project, target_project: project) }
|
|
|
|
it 'counts commits that are on target branch but not on source branch' do
|
|
expect(subject.diverged_commits_count).to eq(0)
|
|
end
|
|
end
|
|
|
|
describe 'caching' do
|
|
before do
|
|
allow(Rails).to receive(:cache).and_return(ActiveSupport::Cache::MemoryStore.new)
|
|
end
|
|
|
|
it 'caches the output' do
|
|
expect(subject).to receive(:compute_diverged_commits_count)
|
|
.once
|
|
.and_return(2)
|
|
|
|
subject.diverged_commits_count
|
|
subject.diverged_commits_count
|
|
end
|
|
|
|
it 'invalidates the cache when the source sha changes' do
|
|
expect(subject).to receive(:compute_diverged_commits_count)
|
|
.twice
|
|
.and_return(2)
|
|
|
|
subject.diverged_commits_count
|
|
allow(subject).to receive(:source_branch_sha).and_return('123abc')
|
|
subject.diverged_commits_count
|
|
end
|
|
|
|
it 'invalidates the cache when the target sha changes' do
|
|
expect(subject).to receive(:compute_diverged_commits_count)
|
|
.twice
|
|
.and_return(2)
|
|
|
|
subject.diverged_commits_count
|
|
allow(subject).to receive(:target_branch_sha).and_return('123abc')
|
|
subject.diverged_commits_count
|
|
end
|
|
end
|
|
end
|
|
|
|
it_behaves_like 'an editable mentionable' do
|
|
subject { create(:merge_request, :simple) }
|
|
|
|
let(:backref_text) { "merge request #{subject.to_reference}" }
|
|
let(:set_mentionable_text) { ->(txt) { subject.description = txt } }
|
|
end
|
|
|
|
it_behaves_like 'a Taskable' do
|
|
subject { create :merge_request, :simple }
|
|
end
|
|
|
|
describe '#commit_shas' do
|
|
before do
|
|
allow(subject.merge_request_diff).to receive(:commit_shas)
|
|
.and_return(['sha1'])
|
|
end
|
|
|
|
it 'delegates to merge request diff' do
|
|
expect(subject.commit_shas).to eq ['sha1']
|
|
end
|
|
end
|
|
|
|
context 'head pipeline' do
|
|
let(:diff_head_sha) { Digest::SHA1.hexdigest(SecureRandom.hex) }
|
|
|
|
before do
|
|
allow(subject).to receive(:diff_head_sha).and_return(diff_head_sha)
|
|
end
|
|
|
|
describe '#head_pipeline' do
|
|
it 'returns nil for MR without head_pipeline_id' do
|
|
subject.update_attribute(:head_pipeline_id, nil)
|
|
|
|
expect(subject.head_pipeline).to be_nil
|
|
end
|
|
|
|
context 'when the source project does not exist' do
|
|
it 'returns nil' do
|
|
allow(subject).to receive(:source_project).and_return(nil)
|
|
|
|
expect(subject.head_pipeline).to be_nil
|
|
end
|
|
end
|
|
end
|
|
|
|
describe '#actual_head_pipeline' do
|
|
it 'returns nil for MR with old pipeline' do
|
|
pipeline = create(:ci_empty_pipeline, sha: 'notlatestsha')
|
|
subject.update_attribute(:head_pipeline_id, pipeline.id)
|
|
|
|
expect(subject.actual_head_pipeline).to be_nil
|
|
end
|
|
|
|
it 'returns the pipeline for MR with recent pipeline' do
|
|
pipeline = create(:ci_empty_pipeline, sha: diff_head_sha)
|
|
subject.update_attribute(:head_pipeline_id, pipeline.id)
|
|
|
|
expect(subject.actual_head_pipeline).to eq(subject.head_pipeline)
|
|
expect(subject.actual_head_pipeline).to eq(pipeline)
|
|
end
|
|
|
|
it 'returns the pipeline for MR with recent merge request pipeline' do
|
|
pipeline = create(:ci_empty_pipeline, sha: 'merge-sha', source_sha: diff_head_sha)
|
|
subject.update_attribute(:head_pipeline_id, pipeline.id)
|
|
|
|
expect(subject.actual_head_pipeline).to eq(subject.head_pipeline)
|
|
expect(subject.actual_head_pipeline).to eq(pipeline)
|
|
end
|
|
|
|
it 'returns nil when source project does not exist' do
|
|
allow(subject).to receive(:source_project).and_return(nil)
|
|
|
|
expect(subject.actual_head_pipeline).to be_nil
|
|
end
|
|
end
|
|
end
|
|
|
|
describe '#merge_pipeline' do
|
|
it 'returns nil when not merged' do
|
|
expect(subject.merge_pipeline).to be_nil
|
|
end
|
|
|
|
context 'when the MR is merged' do
|
|
let(:sha) { subject.target_project.commit.id }
|
|
let(:pipeline) { create(:ci_empty_pipeline, sha: sha, ref: subject.target_branch, project: subject.target_project) }
|
|
|
|
before do
|
|
subject.mark_as_merged!
|
|
subject.update_attribute(:merge_commit_sha, pipeline.sha)
|
|
end
|
|
|
|
it 'returns the post-merge pipeline' do
|
|
expect(subject.merge_pipeline).to eq(pipeline)
|
|
end
|
|
end
|
|
end
|
|
|
|
describe '#has_ci?' do
|
|
let(:merge_request) { build_stubbed(:merge_request) }
|
|
|
|
context 'has ci' do
|
|
it 'returns true if MR has head_pipeline_id and commits' do
|
|
allow(merge_request).to receive_message_chain(:source_project, :ci_service) { nil }
|
|
allow(merge_request).to receive(:head_pipeline_id) { double }
|
|
allow(merge_request).to receive(:has_no_commits?) { false }
|
|
|
|
expect(merge_request.has_ci?).to be(true)
|
|
end
|
|
|
|
it 'returns true if MR has any pipeline and commits' do
|
|
allow(merge_request).to receive_message_chain(:source_project, :ci_service) { nil }
|
|
allow(merge_request).to receive(:head_pipeline_id) { nil }
|
|
allow(merge_request).to receive(:has_no_commits?) { false }
|
|
allow(merge_request).to receive(:all_pipelines) { [double] }
|
|
|
|
expect(merge_request.has_ci?).to be(true)
|
|
end
|
|
|
|
it 'returns true if MR has CI service and commits' do
|
|
allow(merge_request).to receive_message_chain(:source_project, :ci_service) { double }
|
|
allow(merge_request).to receive(:head_pipeline_id) { nil }
|
|
allow(merge_request).to receive(:has_no_commits?) { false }
|
|
allow(merge_request).to receive(:all_pipelines) { [] }
|
|
|
|
expect(merge_request.has_ci?).to be(true)
|
|
end
|
|
end
|
|
|
|
context 'has no ci' do
|
|
it 'returns false if MR has no CI service nor pipeline, and no commits' do
|
|
allow(merge_request).to receive_message_chain(:source_project, :ci_service) { nil }
|
|
allow(merge_request).to receive(:head_pipeline_id) { nil }
|
|
allow(merge_request).to receive(:all_pipelines) { [] }
|
|
allow(merge_request).to receive(:has_no_commits?) { true }
|
|
|
|
expect(merge_request.has_ci?).to be(false)
|
|
end
|
|
end
|
|
end
|
|
|
|
describe '#all_pipelines' do
|
|
shared_examples 'returning pipelines with proper ordering' do
|
|
let!(:all_pipelines) do
|
|
subject.all_commit_shas.map do |sha|
|
|
create(:ci_empty_pipeline,
|
|
project: subject.source_project,
|
|
sha: sha,
|
|
ref: subject.source_branch)
|
|
end
|
|
end
|
|
|
|
it 'returns all pipelines' do
|
|
expect(subject.all_pipelines).not_to be_empty
|
|
expect(subject.all_pipelines).to eq(all_pipelines.reverse)
|
|
end
|
|
end
|
|
|
|
context 'with single merge_request_diffs' do
|
|
it_behaves_like 'returning pipelines with proper ordering'
|
|
end
|
|
|
|
context 'with multiple irrelevant merge_request_diffs' do
|
|
before do
|
|
subject.update(target_branch: 'v1.0.0')
|
|
end
|
|
|
|
it_behaves_like 'returning pipelines with proper ordering'
|
|
end
|
|
|
|
context 'with unsaved merge request' do
|
|
subject { build(:merge_request) }
|
|
|
|
let!(:pipeline) do
|
|
create(:ci_empty_pipeline,
|
|
project: subject.project,
|
|
sha: subject.diff_head_sha,
|
|
ref: subject.source_branch)
|
|
end
|
|
|
|
it 'returns pipelines from diff_head_sha' do
|
|
expect(subject.all_pipelines).to contain_exactly(pipeline)
|
|
end
|
|
end
|
|
|
|
context 'when pipelines exist for the branch and merge request' do
|
|
let(:source_ref) { 'feature' }
|
|
let(:target_ref) { 'master' }
|
|
|
|
let!(:branch_pipeline) do
|
|
create(:ci_pipeline,
|
|
source: :push,
|
|
project: project,
|
|
ref: source_ref,
|
|
sha: shas.second)
|
|
end
|
|
|
|
let!(:detached_merge_request_pipeline) do
|
|
create(:ci_pipeline,
|
|
source: :merge_request_event,
|
|
project: project,
|
|
ref: source_ref,
|
|
sha: shas.second,
|
|
merge_request: merge_request)
|
|
end
|
|
|
|
let(:merge_request) do
|
|
create(:merge_request,
|
|
source_project: project,
|
|
source_branch: source_ref,
|
|
target_project: project,
|
|
target_branch: target_ref)
|
|
end
|
|
|
|
let(:project) { create(:project, :repository) }
|
|
let(:shas) { project.repository.commits(source_ref, limit: 2).map(&:id) }
|
|
|
|
before do
|
|
allow(merge_request).to receive(:all_commit_shas) { shas }
|
|
end
|
|
|
|
it 'returns merge request pipeline first' do
|
|
expect(merge_request.all_pipelines)
|
|
.to eq([detached_merge_request_pipeline,
|
|
branch_pipeline])
|
|
end
|
|
|
|
context 'when there are a branch pipeline and a merge request pipeline' do
|
|
let!(:branch_pipeline_2) do
|
|
create(:ci_pipeline,
|
|
source: :push,
|
|
project: project,
|
|
ref: source_ref,
|
|
sha: shas.first)
|
|
end
|
|
|
|
let!(:detached_merge_request_pipeline_2) do
|
|
create(:ci_pipeline,
|
|
source: :merge_request_event,
|
|
project: project,
|
|
ref: source_ref,
|
|
sha: shas.first,
|
|
merge_request: merge_request)
|
|
end
|
|
|
|
it 'returns merge request pipelines first' do
|
|
expect(merge_request.all_pipelines)
|
|
.to eq([detached_merge_request_pipeline_2,
|
|
detached_merge_request_pipeline,
|
|
branch_pipeline_2,
|
|
branch_pipeline])
|
|
end
|
|
end
|
|
|
|
context 'when there are multiple merge request pipelines from the same branch' do
|
|
let!(:branch_pipeline_2) do
|
|
create(:ci_pipeline,
|
|
source: :push,
|
|
project: project,
|
|
ref: source_ref,
|
|
sha: shas.first)
|
|
end
|
|
|
|
let!(:detached_merge_request_pipeline_2) do
|
|
create(:ci_pipeline,
|
|
source: :merge_request_event,
|
|
project: project,
|
|
ref: source_ref,
|
|
sha: shas.first,
|
|
merge_request: merge_request_2)
|
|
end
|
|
|
|
let(:merge_request_2) do
|
|
create(:merge_request,
|
|
source_project: project,
|
|
source_branch: source_ref,
|
|
target_project: project,
|
|
target_branch: 'stable')
|
|
end
|
|
|
|
before do
|
|
allow(merge_request_2).to receive(:all_commit_shas) { shas }
|
|
end
|
|
|
|
it 'returns only related merge request pipelines' do
|
|
expect(merge_request.all_pipelines)
|
|
.to eq([detached_merge_request_pipeline,
|
|
branch_pipeline_2,
|
|
branch_pipeline])
|
|
|
|
expect(merge_request_2.all_pipelines)
|
|
.to eq([detached_merge_request_pipeline_2,
|
|
branch_pipeline_2,
|
|
branch_pipeline])
|
|
end
|
|
end
|
|
|
|
context 'when detached merge request pipeline is run on head ref of the merge request' do
|
|
let!(:detached_merge_request_pipeline) do
|
|
create(:ci_pipeline,
|
|
source: :merge_request_event,
|
|
project: project,
|
|
ref: merge_request.ref_path,
|
|
sha: shas.second,
|
|
merge_request: merge_request)
|
|
end
|
|
|
|
it 'sets the head ref of the merge request to the pipeline ref' do
|
|
expect(detached_merge_request_pipeline.ref).to match(%r{refs/merge-requests/\d+/head})
|
|
end
|
|
|
|
it 'includes the detached merge request pipeline even though the ref is custom path' do
|
|
expect(merge_request.all_pipelines).to include(detached_merge_request_pipeline)
|
|
end
|
|
end
|
|
end
|
|
end
|
|
|
|
describe '#update_head_pipeline' do
|
|
subject { merge_request.update_head_pipeline }
|
|
|
|
let(:merge_request) { create(:merge_request) }
|
|
|
|
context 'when there is a pipeline with the diff head sha' do
|
|
let!(:pipeline) do
|
|
create(:ci_empty_pipeline,
|
|
project: merge_request.project,
|
|
sha: merge_request.diff_head_sha,
|
|
ref: merge_request.source_branch)
|
|
end
|
|
|
|
it 'updates the head pipeline' do
|
|
expect { subject }
|
|
.to change { merge_request.reload.head_pipeline }
|
|
.from(nil).to(pipeline)
|
|
end
|
|
|
|
context 'when merge request has already had head pipeline' do
|
|
before do
|
|
merge_request.update!(head_pipeline: pipeline)
|
|
end
|
|
|
|
context 'when failed to find an actual head pipeline' do
|
|
before do
|
|
allow(merge_request).to receive(:find_actual_head_pipeline) { }
|
|
end
|
|
|
|
it 'does not update the current head pipeline' do
|
|
expect { subject }
|
|
.not_to change { merge_request.reload.head_pipeline }
|
|
end
|
|
end
|
|
end
|
|
end
|
|
|
|
context 'when detached merge request pipeline is run on head ref of the merge request' do
|
|
let!(:pipeline) do
|
|
create(:ci_pipeline,
|
|
source: :merge_request_event,
|
|
project: merge_request.source_project,
|
|
ref: merge_request.ref_path,
|
|
sha: sha,
|
|
merge_request: merge_request)
|
|
end
|
|
|
|
let(:sha) { merge_request.diff_head_sha }
|
|
|
|
it 'sets the head ref of the merge request to the pipeline ref' do
|
|
expect(pipeline.ref).to match(%r{refs/merge-requests/\d+/head})
|
|
end
|
|
|
|
it 'updates correctly even though the target branch name of the merge request is different from the pipeline ref' do
|
|
expect { subject }
|
|
.to change { merge_request.reload.head_pipeline }
|
|
.from(nil).to(pipeline)
|
|
end
|
|
|
|
context 'when sha is not HEAD of the source branch' do
|
|
let(:sha) { merge_request.diff_base_sha }
|
|
|
|
it 'does not update head pipeline' do
|
|
expect { subject }.not_to change { merge_request.reload.head_pipeline }
|
|
end
|
|
end
|
|
end
|
|
|
|
context 'when there are no pipelines with the diff head sha' do
|
|
it 'does not update the head pipeline' do
|
|
expect { subject }
|
|
.not_to change { merge_request.reload.head_pipeline }
|
|
end
|
|
end
|
|
end
|
|
|
|
describe '#has_test_reports?' do
|
|
subject { merge_request.has_test_reports? }
|
|
|
|
let(:project) { create(:project, :repository) }
|
|
|
|
context 'when head pipeline has test reports' do
|
|
let(:merge_request) { create(:merge_request, :with_test_reports, source_project: project) }
|
|
|
|
it { is_expected.to be_truthy }
|
|
end
|
|
|
|
context 'when head pipeline does not have test reports' do
|
|
let(:merge_request) { create(:merge_request, source_project: project) }
|
|
|
|
it { is_expected.to be_falsey }
|
|
end
|
|
end
|
|
|
|
describe '#calculate_reactive_cache' do
|
|
let(:project) { create(:project, :repository) }
|
|
let(:merge_request) { create(:merge_request, source_project: project) }
|
|
subject { merge_request.calculate_reactive_cache(service_class_name) }
|
|
|
|
context 'when given an unknown service class name' do
|
|
let(:service_class_name) { 'Integer' }
|
|
|
|
it 'raises a NameError exception' do
|
|
expect { subject }.to raise_error(NameError, service_class_name)
|
|
end
|
|
end
|
|
|
|
context 'when given a known service class name' do
|
|
let(:service_class_name) { 'Ci::CompareTestReportsService' }
|
|
|
|
it 'does not raises a NameError exception' do
|
|
allow_any_instance_of(service_class_name.constantize).to receive(:execute).and_return(nil)
|
|
|
|
expect { subject }.not_to raise_error
|
|
end
|
|
end
|
|
end
|
|
|
|
describe '#compare_test_reports' do
|
|
subject { merge_request.compare_test_reports }
|
|
|
|
let(:project) { create(:project, :repository) }
|
|
let(:merge_request) { create(:merge_request, source_project: project) }
|
|
|
|
let!(:base_pipeline) do
|
|
create(:ci_pipeline,
|
|
:with_test_reports,
|
|
project: project,
|
|
ref: merge_request.target_branch,
|
|
sha: merge_request.diff_base_sha)
|
|
end
|
|
|
|
before do
|
|
merge_request.update!(head_pipeline_id: head_pipeline.id)
|
|
end
|
|
|
|
context 'when head pipeline has test reports' do
|
|
let!(:head_pipeline) do
|
|
create(:ci_pipeline,
|
|
:with_test_reports,
|
|
project: project,
|
|
ref: merge_request.source_branch,
|
|
sha: merge_request.diff_head_sha)
|
|
end
|
|
|
|
context 'when reactive cache worker is parsing asynchronously' do
|
|
it 'returns status' do
|
|
expect(subject[:status]).to eq(:parsing)
|
|
end
|
|
end
|
|
|
|
context 'when reactive cache worker is inline' do
|
|
before do
|
|
synchronous_reactive_cache(merge_request)
|
|
end
|
|
|
|
it 'returns status and data' do
|
|
expect_any_instance_of(Ci::CompareTestReportsService)
|
|
.to receive(:execute).with(base_pipeline, head_pipeline).and_call_original
|
|
|
|
subject
|
|
end
|
|
|
|
context 'when cached results is not latest' do
|
|
before do
|
|
allow_any_instance_of(Ci::CompareTestReportsService)
|
|
.to receive(:latest?).and_return(false)
|
|
end
|
|
|
|
it 'raises and InvalidateReactiveCache error' do
|
|
expect { subject }.to raise_error(ReactiveCaching::InvalidateReactiveCache)
|
|
end
|
|
end
|
|
end
|
|
end
|
|
|
|
context 'when head pipeline does not have test reports' do
|
|
let!(:head_pipeline) do
|
|
create(:ci_pipeline,
|
|
project: project,
|
|
ref: merge_request.source_branch,
|
|
sha: merge_request.diff_head_sha)
|
|
end
|
|
|
|
it 'returns status and error message' do
|
|
expect(subject[:status]).to eq(:error)
|
|
expect(subject[:status_reason]).to eq('This merge request does not have test reports')
|
|
end
|
|
end
|
|
end
|
|
|
|
describe '#all_commit_shas' do
|
|
context 'when merge request is persisted' do
|
|
let(:all_commit_shas) do
|
|
subject.merge_request_diffs.flat_map(&:commits).map(&:sha).uniq
|
|
end
|
|
|
|
shared_examples 'returning all SHA' do
|
|
it 'returns all SHAs from all merge_request_diffs' do
|
|
expect(subject.merge_request_diffs.size).to eq(2)
|
|
expect(subject.all_commit_shas).to match_array(all_commit_shas)
|
|
end
|
|
end
|
|
|
|
context 'with a completely different branch' do
|
|
before do
|
|
subject.update(target_branch: 'csv')
|
|
end
|
|
|
|
it_behaves_like 'returning all SHA'
|
|
end
|
|
|
|
context 'with a branch having no difference' do
|
|
before do
|
|
subject.update(target_branch: 'branch-merged')
|
|
subject.reload # make sure commits were not cached
|
|
end
|
|
|
|
it_behaves_like 'returning all SHA'
|
|
end
|
|
end
|
|
|
|
context 'when merge request is not persisted' do
|
|
context 'when compare commits are set in the service' do
|
|
let(:commit) { spy('commit') }
|
|
|
|
subject do
|
|
build(:merge_request, compare_commits: [commit, commit])
|
|
end
|
|
|
|
it 'returns commits from compare commits temporary data' do
|
|
expect(subject.all_commit_shas).to eq [commit, commit]
|
|
end
|
|
end
|
|
|
|
context 'when compare commits are not set in the service' do
|
|
subject { build(:merge_request) }
|
|
|
|
it 'returns array with diff head sha element only' do
|
|
expect(subject.all_commit_shas).to eq [subject.diff_head_sha]
|
|
end
|
|
end
|
|
end
|
|
end
|
|
|
|
describe '#short_merge_commit_sha' do
|
|
let(:merge_request) { build_stubbed(:merge_request) }
|
|
|
|
it 'returns short id when there is a merge_commit_sha' do
|
|
merge_request.merge_commit_sha = 'f7ce827c314c9340b075657fd61c789fb01cf74d'
|
|
|
|
expect(merge_request.short_merge_commit_sha).to eq('f7ce827c')
|
|
end
|
|
|
|
it 'returns nil when there is no merge_commit_sha' do
|
|
merge_request.merge_commit_sha = nil
|
|
|
|
expect(merge_request.short_merge_commit_sha).to be_nil
|
|
end
|
|
end
|
|
|
|
describe '#can_be_reverted?' do
|
|
context 'when there is no merge_commit for the MR' do
|
|
before do
|
|
subject.metrics.update!(merged_at: Time.now.utc)
|
|
end
|
|
|
|
it 'returns false' do
|
|
expect(subject.can_be_reverted?(nil)).to be_falsey
|
|
end
|
|
end
|
|
|
|
context 'when the MR has been merged' do
|
|
before do
|
|
MergeRequests::MergeService
|
|
.new(subject.target_project, subject.author)
|
|
.execute(subject)
|
|
end
|
|
|
|
context 'when there is no revert commit' do
|
|
it 'returns true' do
|
|
expect(subject.can_be_reverted?(nil)).to be_truthy
|
|
end
|
|
end
|
|
|
|
context 'when there is no merged_at for the MR' do
|
|
before do
|
|
subject.metrics.update!(merged_at: nil)
|
|
end
|
|
|
|
it 'returns true' do
|
|
expect(subject.can_be_reverted?(nil)).to be_truthy
|
|
end
|
|
end
|
|
|
|
context 'when there is a revert commit' do
|
|
let(:current_user) { subject.author }
|
|
let(:branch) { subject.target_branch }
|
|
let(:project) { subject.target_project }
|
|
|
|
let(:revert_commit_id) do
|
|
params = {
|
|
commit: subject.merge_commit,
|
|
branch_name: branch,
|
|
start_branch: branch
|
|
}
|
|
|
|
Commits::RevertService.new(project, current_user, params).execute[:result]
|
|
end
|
|
|
|
before do
|
|
project.add_maintainer(current_user)
|
|
|
|
ProcessCommitWorker.new.perform(project.id,
|
|
current_user.id,
|
|
project.commit(revert_commit_id).to_hash,
|
|
project.default_branch == branch)
|
|
end
|
|
|
|
context 'but merged at timestamp cannot be found' do
|
|
before do
|
|
allow(subject).to receive(:merged_at) { nil }
|
|
end
|
|
|
|
it 'returns false' do
|
|
expect(subject.can_be_reverted?(current_user)).to be_falsey
|
|
end
|
|
end
|
|
|
|
context 'when the revert commit is mentioned in a note after the MR was merged' do
|
|
it 'returns false' do
|
|
expect(subject.can_be_reverted?(current_user)).to be_falsey
|
|
end
|
|
end
|
|
|
|
context 'when there is no merged_at for the MR' do
|
|
before do
|
|
subject.metrics.update!(merged_at: nil)
|
|
end
|
|
|
|
it 'returns false' do
|
|
expect(subject.can_be_reverted?(current_user)).to be_falsey
|
|
end
|
|
end
|
|
|
|
context 'when the revert commit is mentioned in a note just before the MR was merged' do
|
|
before do
|
|
subject.notes.last.update!(created_at: subject.metrics.merged_at - 30.seconds)
|
|
end
|
|
|
|
it 'returns false' do
|
|
expect(subject.can_be_reverted?(current_user)).to be_falsey
|
|
end
|
|
end
|
|
|
|
context 'when the revert commit is mentioned in a note long before the MR was merged' do
|
|
before do
|
|
subject.notes.last.update!(created_at: subject.metrics.merged_at - 2.minutes)
|
|
end
|
|
|
|
it 'returns true' do
|
|
expect(subject.can_be_reverted?(current_user)).to be_truthy
|
|
end
|
|
end
|
|
end
|
|
end
|
|
end
|
|
|
|
describe '#merged_at' do
|
|
context 'when MR is not merged' do
|
|
let(:merge_request) { create(:merge_request, :closed) }
|
|
|
|
it 'returns nil' do
|
|
expect(merge_request.merged_at).to be_nil
|
|
end
|
|
end
|
|
|
|
context 'when metrics has merged_at data' do
|
|
let(:merge_request) { create(:merge_request, :merged) }
|
|
|
|
before do
|
|
merge_request.metrics.update!(merged_at: 1.day.ago)
|
|
end
|
|
|
|
it 'returns metrics merged_at' do
|
|
expect(merge_request.merged_at).to eq(merge_request.metrics.merged_at)
|
|
end
|
|
end
|
|
|
|
context 'when merged event is persisted, but no metrics merged_at is persisted' do
|
|
let(:user) { create(:user) }
|
|
let(:merge_request) { create(:merge_request, :merged) }
|
|
|
|
before do
|
|
EventCreateService.new.merge_mr(merge_request, user)
|
|
end
|
|
|
|
it 'returns merged event creation date' do
|
|
expect(merge_request.merge_event).to be_persisted
|
|
expect(merge_request.merged_at).to eq(merge_request.merge_event.created_at)
|
|
end
|
|
end
|
|
|
|
context 'when merging note is persisted, but no metrics or merge event exists' do
|
|
let(:user) { create(:user) }
|
|
let(:merge_request) { create(:merge_request, :merged) }
|
|
|
|
before do
|
|
merge_request.metrics.destroy!
|
|
|
|
SystemNoteService.change_status(merge_request,
|
|
merge_request.target_project,
|
|
user,
|
|
merge_request.state, nil)
|
|
end
|
|
|
|
it 'returns merging note creation date' do
|
|
expect(merge_request.reload.metrics).to be_nil
|
|
expect(merge_request.merge_event).to be_nil
|
|
expect(merge_request.notes.count).to eq(1)
|
|
expect(merge_request.merged_at).to eq(merge_request.notes.first.created_at)
|
|
end
|
|
end
|
|
end
|
|
|
|
describe '#participants' do
|
|
let(:project) { create(:project, :public) }
|
|
|
|
let(:mr) do
|
|
create(:merge_request, source_project: project, target_project: project)
|
|
end
|
|
|
|
let!(:note1) do
|
|
create(:note_on_merge_request, noteable: mr, project: project, note: 'a')
|
|
end
|
|
|
|
let!(:note2) do
|
|
create(:note_on_merge_request, noteable: mr, project: project, note: 'b')
|
|
end
|
|
|
|
it 'includes the merge request author' do
|
|
expect(mr.participants).to include(mr.author)
|
|
end
|
|
|
|
it 'includes the authors of the notes' do
|
|
expect(mr.participants).to include(note1.author, note2.author)
|
|
end
|
|
end
|
|
|
|
describe 'cached counts' do
|
|
it 'updates when assignees change' do
|
|
user1 = create(:user)
|
|
user2 = create(:user)
|
|
mr = create(:merge_request, assignees: [user1])
|
|
mr.project.add_developer(user1)
|
|
mr.project.add_developer(user2)
|
|
|
|
expect(user1.assigned_open_merge_requests_count).to eq(1)
|
|
expect(user2.assigned_open_merge_requests_count).to eq(0)
|
|
|
|
mr.assignees = [user2]
|
|
|
|
expect(user1.assigned_open_merge_requests_count).to eq(0)
|
|
expect(user2.assigned_open_merge_requests_count).to eq(1)
|
|
end
|
|
end
|
|
|
|
describe '#merge_async' do
|
|
it 'enqueues MergeWorker job and updates merge_jid' do
|
|
merge_request = create(:merge_request)
|
|
user_id = double(:user_id)
|
|
params = {}
|
|
merge_jid = 'hash-123'
|
|
|
|
expect(MergeWorker).to receive(:perform_async).with(merge_request.id, user_id, params) do
|
|
merge_jid
|
|
end
|
|
|
|
merge_request.merge_async(user_id, params)
|
|
|
|
expect(merge_request.reload.merge_jid).to eq(merge_jid)
|
|
end
|
|
end
|
|
|
|
describe '#rebase_async' do
|
|
let(:merge_request) { create(:merge_request) }
|
|
let(:user_id) { double(:user_id) }
|
|
let(:rebase_jid) { 'rebase-jid' }
|
|
|
|
subject(:execute) { merge_request.rebase_async(user_id) }
|
|
|
|
it 'atomically enqueues a RebaseWorker job and updates rebase_jid' do
|
|
expect(RebaseWorker)
|
|
.to receive(:perform_async)
|
|
.with(merge_request.id, user_id)
|
|
.and_return(rebase_jid)
|
|
|
|
expect(merge_request).to receive(:lock!).and_call_original
|
|
|
|
execute
|
|
|
|
expect(merge_request.rebase_jid).to eq(rebase_jid)
|
|
end
|
|
|
|
it 'refuses to enqueue a job if a rebase is in progress' do
|
|
merge_request.update_column(:rebase_jid, rebase_jid)
|
|
|
|
expect(RebaseWorker).not_to receive(:perform_async)
|
|
expect(Gitlab::SidekiqStatus)
|
|
.to receive(:running?)
|
|
.with(rebase_jid)
|
|
.and_return(true)
|
|
|
|
expect { execute }.to raise_error(ActiveRecord::StaleObjectError)
|
|
end
|
|
|
|
it 'refuses to enqueue a job if the MR is not open' do
|
|
merge_request.update_column(:state, 'foo')
|
|
|
|
expect(RebaseWorker).not_to receive(:perform_async)
|
|
|
|
expect { execute }.to raise_error(ActiveRecord::StaleObjectError)
|
|
end
|
|
end
|
|
|
|
describe '#mergeable?' do
|
|
let(:project) { create(:project) }
|
|
|
|
subject { create(:merge_request, source_project: project) }
|
|
|
|
it 'returns false if #mergeable_state? is false' do
|
|
expect(subject).to receive(:mergeable_state?) { false }
|
|
|
|
expect(subject.mergeable?).to be_falsey
|
|
end
|
|
|
|
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_mergeability)
|
|
expect(subject).to receive(:can_be_merged?) { true }
|
|
|
|
expect(subject.mergeable?).to be_truthy
|
|
end
|
|
end
|
|
|
|
describe '#mergeable_state?' do
|
|
let(:project) { create(:project, :repository) }
|
|
|
|
subject { create(:merge_request, source_project: project) }
|
|
|
|
it 'checks if merge request can be merged' do
|
|
allow(subject).to receive(:mergeable_ci_state?) { true }
|
|
expect(subject).to receive(:check_mergeability)
|
|
|
|
subject.mergeable?
|
|
end
|
|
|
|
context 'when not open' do
|
|
before do
|
|
subject.close
|
|
end
|
|
|
|
it 'returns false' do
|
|
expect(subject.mergeable_state?).to be_falsey
|
|
end
|
|
end
|
|
|
|
context 'when working in progress' do
|
|
before do
|
|
subject.title = 'WIP MR'
|
|
end
|
|
|
|
it 'returns false' do
|
|
expect(subject.mergeable_state?).to be_falsey
|
|
end
|
|
end
|
|
|
|
context 'when broken' do
|
|
before do
|
|
allow(subject).to receive(:broken?) { true }
|
|
end
|
|
|
|
it 'returns false' do
|
|
expect(subject.mergeable_state?).to be_falsey
|
|
end
|
|
end
|
|
|
|
context 'when failed' do
|
|
context 'when #mergeable_ci_state? is false' do
|
|
before do
|
|
allow(subject).to receive(:mergeable_ci_state?) { false }
|
|
end
|
|
|
|
it 'returns false' do
|
|
expect(subject.mergeable_state?).to be_falsey
|
|
end
|
|
end
|
|
|
|
context 'when #mergeable_discussions_state? is false' do
|
|
before do
|
|
allow(subject).to receive(:mergeable_discussions_state?) { false }
|
|
end
|
|
|
|
it 'returns false' do
|
|
expect(subject.mergeable_state?).to be_falsey
|
|
end
|
|
|
|
it 'returns true when skipping discussions check' do
|
|
expect(subject.mergeable_state?(skip_discussions_check: true)).to be(true)
|
|
end
|
|
end
|
|
end
|
|
end
|
|
|
|
describe '#mergeable_ci_state?' do
|
|
let(:project) { create(:project, only_allow_merge_if_pipeline_succeeds: true) }
|
|
let(:pipeline) { create(:ci_empty_pipeline) }
|
|
|
|
subject { build(:merge_request, target_project: project) }
|
|
|
|
context 'when it is only allowed to merge when build is green' do
|
|
context 'and a failed pipeline is associated' do
|
|
before do
|
|
pipeline.update(status: 'failed', sha: subject.diff_head_sha)
|
|
allow(subject).to receive(:head_pipeline) { pipeline }
|
|
end
|
|
|
|
it { expect(subject.mergeable_ci_state?).to be_falsey }
|
|
end
|
|
|
|
context 'and a successful pipeline is associated' do
|
|
before do
|
|
pipeline.update(status: 'success', sha: subject.diff_head_sha)
|
|
allow(subject).to receive(:head_pipeline) { pipeline }
|
|
end
|
|
|
|
it { expect(subject.mergeable_ci_state?).to be_truthy }
|
|
end
|
|
|
|
context 'and a skipped pipeline is associated' do
|
|
before do
|
|
pipeline.update(status: 'skipped', sha: subject.diff_head_sha)
|
|
allow(subject).to receive(:head_pipeline) { pipeline }
|
|
end
|
|
|
|
it { expect(subject.mergeable_ci_state?).to be_truthy }
|
|
end
|
|
|
|
context 'when no pipeline is associated' do
|
|
before do
|
|
allow(subject).to receive(:head_pipeline) { nil }
|
|
end
|
|
|
|
it { expect(subject.mergeable_ci_state?).to be_falsey }
|
|
end
|
|
end
|
|
|
|
context 'when merges are not restricted to green builds' do
|
|
subject { build(:merge_request, target_project: create(:project, only_allow_merge_if_pipeline_succeeds: false)) }
|
|
|
|
context 'and a failed pipeline is associated' do
|
|
before do
|
|
pipeline.statuses << create(:commit_status, status: 'failed', project: project)
|
|
allow(subject).to receive(:head_pipeline) { pipeline }
|
|
end
|
|
|
|
it { expect(subject.mergeable_ci_state?).to be_truthy }
|
|
end
|
|
|
|
context 'when no pipeline is associated' do
|
|
before do
|
|
allow(subject).to receive(:head_pipeline) { nil }
|
|
end
|
|
|
|
it { expect(subject.mergeable_ci_state?).to be_truthy }
|
|
end
|
|
end
|
|
end
|
|
|
|
describe '#mergeable_discussions_state?' do
|
|
let(:merge_request) { create(:merge_request_with_diff_notes, source_project: project) }
|
|
|
|
context 'when project.only_allow_merge_if_all_discussions_are_resolved == true' do
|
|
let(:project) { create(:project, :repository, only_allow_merge_if_all_discussions_are_resolved: true) }
|
|
|
|
context 'with all discussions resolved' do
|
|
before do
|
|
merge_request.discussions.each { |d| d.resolve!(merge_request.author) }
|
|
end
|
|
|
|
it 'returns true' do
|
|
expect(merge_request.mergeable_discussions_state?).to be_truthy
|
|
end
|
|
end
|
|
|
|
context 'with unresolved discussions' do
|
|
before do
|
|
merge_request.discussions.each(&:unresolve!)
|
|
end
|
|
|
|
it 'returns false' do
|
|
expect(merge_request.mergeable_discussions_state?).to be_falsey
|
|
end
|
|
end
|
|
|
|
context 'with no discussions' do
|
|
before do
|
|
merge_request.notes.destroy_all # rubocop: disable DestroyAll
|
|
end
|
|
|
|
it 'returns true' do
|
|
expect(merge_request.mergeable_discussions_state?).to be_truthy
|
|
end
|
|
end
|
|
end
|
|
|
|
context 'when project.only_allow_merge_if_all_discussions_are_resolved == false' do
|
|
let(:project) { create(:project, :repository, only_allow_merge_if_all_discussions_are_resolved: false) }
|
|
|
|
context 'with unresolved discussions' do
|
|
before do
|
|
merge_request.discussions.each(&:unresolve!)
|
|
end
|
|
|
|
it 'returns true' do
|
|
expect(merge_request.mergeable_discussions_state?).to be_truthy
|
|
end
|
|
end
|
|
end
|
|
end
|
|
|
|
describe "#environments_for" do
|
|
let(:project) { create(:project, :repository) }
|
|
let(:user) { project.creator }
|
|
let(:merge_request) { create(:merge_request, source_project: project) }
|
|
|
|
before do
|
|
merge_request.source_project.add_maintainer(user)
|
|
merge_request.target_project.add_maintainer(user)
|
|
end
|
|
|
|
context 'with multiple environments' do
|
|
let(:environments) { create_list(:environment, 3, project: project) }
|
|
|
|
before do
|
|
create(:deployment, :success, environment: environments.first, ref: 'master', sha: project.commit('master').id)
|
|
create(:deployment, :success, environment: environments.second, ref: 'feature', sha: project.commit('feature').id)
|
|
end
|
|
|
|
it 'selects deployed environments' do
|
|
expect(merge_request.environments_for(user)).to contain_exactly(environments.first)
|
|
end
|
|
end
|
|
|
|
context 'with environments on source project' do
|
|
let(:source_project) { fork_project(project, nil, repository: true) }
|
|
|
|
let(:merge_request) do
|
|
create(:merge_request,
|
|
source_project: source_project, source_branch: 'feature',
|
|
target_project: project)
|
|
end
|
|
|
|
let(:source_environment) { create(:environment, project: source_project) }
|
|
|
|
before do
|
|
create(:deployment, :success, environment: source_environment, ref: 'feature', sha: merge_request.diff_head_sha)
|
|
end
|
|
|
|
it 'selects deployed environments' do
|
|
expect(merge_request.environments_for(user)).to contain_exactly(source_environment)
|
|
end
|
|
|
|
context 'with environments on target project' do
|
|
let(:target_environment) { create(:environment, project: project) }
|
|
|
|
before do
|
|
create(:deployment, :success, environment: target_environment, tag: true, sha: merge_request.diff_head_sha)
|
|
end
|
|
|
|
it 'selects deployed environments' do
|
|
expect(merge_request.environments_for(user)).to contain_exactly(source_environment, target_environment)
|
|
end
|
|
end
|
|
end
|
|
|
|
context 'without a diff_head_commit' do
|
|
before do
|
|
expect(merge_request).to receive(:diff_head_commit).and_return(nil)
|
|
end
|
|
|
|
it 'returns an empty array' do
|
|
expect(merge_request.environments_for(user)).to be_empty
|
|
end
|
|
end
|
|
end
|
|
|
|
describe "#environments" do
|
|
subject { merge_request.environments }
|
|
|
|
let(:merge_request) { create(:merge_request, source_branch: 'feature', target_branch: 'master') }
|
|
let(:project) { merge_request.project }
|
|
|
|
let(:pipeline) do
|
|
create(:ci_pipeline,
|
|
source: :merge_request_event,
|
|
merge_request: merge_request, project: project,
|
|
sha: merge_request.diff_head_sha,
|
|
merge_requests_as_head_pipeline: [merge_request])
|
|
end
|
|
|
|
let!(:job) { create(:ci_build, :start_review_app, pipeline: pipeline, project: project) }
|
|
|
|
it 'returns environments' do
|
|
is_expected.to eq(pipeline.environments)
|
|
expect(subject.count).to be(1)
|
|
end
|
|
|
|
context 'when pipeline is not associated with environments' do
|
|
let!(:job) { create(:ci_build, pipeline: pipeline, project: project) }
|
|
|
|
it 'returns empty array' do
|
|
is_expected.to be_empty
|
|
end
|
|
end
|
|
|
|
context 'when pipeline is not a pipeline for merge request' do
|
|
let(:pipeline) do
|
|
create(:ci_pipeline,
|
|
project: project,
|
|
ref: 'feature',
|
|
sha: merge_request.diff_head_sha,
|
|
merge_requests_as_head_pipeline: [merge_request])
|
|
end
|
|
|
|
it 'returns empty relation' do
|
|
is_expected.to be_empty
|
|
end
|
|
end
|
|
end
|
|
|
|
describe "#reload_diff" do
|
|
it 'calls MergeRequests::ReloadDiffsService#execute with correct params' do
|
|
user = create(:user)
|
|
service = instance_double(MergeRequests::ReloadDiffsService, execute: nil)
|
|
|
|
expect(MergeRequests::ReloadDiffsService)
|
|
.to receive(:new).with(subject, user)
|
|
.and_return(service)
|
|
|
|
subject.reload_diff(user)
|
|
|
|
expect(service).to have_received(:execute)
|
|
end
|
|
|
|
context 'when using the after_update hook to update' do
|
|
context 'when the branches are updated' do
|
|
it 'uses the new heads to generate the diff' do
|
|
expect { subject.update!(source_branch: subject.target_branch, target_branch: subject.source_branch) }
|
|
.to change { subject.merge_request_diff.start_commit_sha }
|
|
.and change { subject.merge_request_diff.head_commit_sha }
|
|
end
|
|
end
|
|
end
|
|
end
|
|
|
|
describe '#update_diff_discussion_positions' do
|
|
let(:discussion) { create(:diff_note_on_merge_request, project: subject.project, noteable: subject).to_discussion }
|
|
let(:commit) { subject.project.commit(sample_commit.id) }
|
|
let(:old_diff_refs) { subject.diff_refs }
|
|
|
|
before do
|
|
# Update merge_request_diff so that #diff_refs will return commit.diff_refs
|
|
allow(subject).to receive(:create_merge_request_diff) do
|
|
subject.merge_request_diffs.create(
|
|
base_commit_sha: commit.parent_id,
|
|
start_commit_sha: commit.parent_id,
|
|
head_commit_sha: commit.sha
|
|
)
|
|
|
|
subject.reload_merge_request_diff
|
|
end
|
|
end
|
|
|
|
it "updates diff discussion positions" do
|
|
expect(Discussions::UpdateDiffPositionService).to receive(:new).with(
|
|
subject.project,
|
|
subject.author,
|
|
old_diff_refs: old_diff_refs,
|
|
new_diff_refs: commit.diff_refs,
|
|
paths: discussion.position.paths
|
|
).and_call_original
|
|
|
|
expect_any_instance_of(Discussions::UpdateDiffPositionService).to receive(:execute).with(discussion).and_call_original
|
|
expect_any_instance_of(DiffNote).to receive(:save).once
|
|
|
|
subject.update_diff_discussion_positions(old_diff_refs: old_diff_refs,
|
|
new_diff_refs: commit.diff_refs,
|
|
current_user: subject.author)
|
|
end
|
|
|
|
context 'when resolve_outdated_diff_discussions is set' do
|
|
before do
|
|
discussion
|
|
|
|
subject.project.update!(resolve_outdated_diff_discussions: true)
|
|
end
|
|
|
|
it 'calls MergeRequests::ResolvedDiscussionNotificationService' do
|
|
expect_any_instance_of(MergeRequests::ResolvedDiscussionNotificationService)
|
|
.to receive(:execute).with(subject)
|
|
|
|
subject.update_diff_discussion_positions(old_diff_refs: old_diff_refs,
|
|
new_diff_refs: commit.diff_refs,
|
|
current_user: subject.author)
|
|
end
|
|
end
|
|
end
|
|
|
|
describe '#branch_merge_base_commit' do
|
|
context 'source and target branch exist' do
|
|
it { expect(subject.branch_merge_base_commit.sha).to eq('ae73cb07c9eeaf35924a10f713b364d32b2dd34f') }
|
|
it { expect(subject.branch_merge_base_commit).to be_a(Commit) }
|
|
end
|
|
|
|
context 'when the target branch does not exist' do
|
|
before do
|
|
subject.project.repository.rm_branch(subject.author, subject.target_branch)
|
|
subject.clear_memoized_shas
|
|
end
|
|
|
|
it 'returns nil' do
|
|
expect(subject.branch_merge_base_commit).to be_nil
|
|
end
|
|
end
|
|
end
|
|
|
|
describe "#diff_refs" do
|
|
context "with diffs" do
|
|
subject { create(:merge_request, :with_diffs) }
|
|
let(:expected_diff_refs) do
|
|
Gitlab::Diff::DiffRefs.new(
|
|
base_sha: subject.merge_request_diff.base_commit_sha,
|
|
start_sha: subject.merge_request_diff.start_commit_sha,
|
|
head_sha: subject.merge_request_diff.head_commit_sha
|
|
)
|
|
end
|
|
|
|
it "does not touch the repository" do
|
|
subject # Instantiate the object
|
|
|
|
expect_any_instance_of(Repository).not_to receive(:commit)
|
|
|
|
subject.diff_refs
|
|
end
|
|
|
|
it "returns expected diff_refs" do
|
|
expect(subject.diff_refs).to eq(expected_diff_refs)
|
|
end
|
|
|
|
context 'when importing' do
|
|
before do
|
|
subject.importing = true
|
|
end
|
|
|
|
it "returns MR diff_refs" do
|
|
expect(subject.diff_refs).to eq(expected_diff_refs)
|
|
end
|
|
end
|
|
end
|
|
end
|
|
|
|
describe "#source_project_missing?" do
|
|
let(:project) { create(:project) }
|
|
let(:forked_project) { fork_project(project) }
|
|
let(:user) { create(:user) }
|
|
let(:unlink_project) { Projects::UnlinkForkService.new(forked_project, user) }
|
|
|
|
context "when the fork exists" do
|
|
let(:merge_request) do
|
|
create(:merge_request,
|
|
source_project: forked_project,
|
|
target_project: project)
|
|
end
|
|
|
|
it { expect(merge_request.source_project_missing?).to be_falsey }
|
|
end
|
|
|
|
context "when the source project is the same as the target project" do
|
|
let(:merge_request) { create(:merge_request, source_project: project) }
|
|
|
|
it { expect(merge_request.source_project_missing?).to be_falsey }
|
|
end
|
|
|
|
context "when the fork does not exist" do
|
|
let!(:merge_request) do
|
|
create(:merge_request,
|
|
source_project: forked_project,
|
|
target_project: project)
|
|
end
|
|
|
|
it "returns true" do
|
|
unlink_project.execute
|
|
merge_request.reload
|
|
|
|
expect(merge_request.source_project_missing?).to be_truthy
|
|
end
|
|
end
|
|
end
|
|
|
|
describe '#merge_ongoing?' do
|
|
it 'returns true when the merge request is locked' do
|
|
merge_request = build_stubbed(:merge_request, state: :locked)
|
|
|
|
expect(merge_request.merge_ongoing?).to be(true)
|
|
end
|
|
|
|
it 'returns true when merge_id, MR is not merged and it has no running job' do
|
|
merge_request = build_stubbed(:merge_request, state: :open, merge_jid: 'foo')
|
|
allow(Gitlab::SidekiqStatus).to receive(:running?).with('foo') { true }
|
|
|
|
expect(merge_request.merge_ongoing?).to be(true)
|
|
end
|
|
|
|
it 'returns false when merge_jid is nil' do
|
|
merge_request = build_stubbed(:merge_request, state: :open, merge_jid: nil)
|
|
|
|
expect(merge_request.merge_ongoing?).to be(false)
|
|
end
|
|
|
|
it 'returns false if MR is merged' do
|
|
merge_request = build_stubbed(:merge_request, state: :merged, merge_jid: 'foo')
|
|
|
|
expect(merge_request.merge_ongoing?).to be(false)
|
|
end
|
|
|
|
it 'returns false if there is no merge job running' do
|
|
merge_request = build_stubbed(:merge_request, state: :open, merge_jid: 'foo')
|
|
allow(Gitlab::SidekiqStatus).to receive(:running?).with('foo') { false }
|
|
|
|
expect(merge_request.merge_ongoing?).to be(false)
|
|
end
|
|
end
|
|
|
|
describe "#closed_without_fork?" do
|
|
let(:project) { create(:project) }
|
|
let(:forked_project) { fork_project(project) }
|
|
let(:user) { create(:user) }
|
|
let(:unlink_project) { Projects::UnlinkForkService.new(forked_project, user) }
|
|
|
|
context "when the merge request is closed" do
|
|
let(:closed_merge_request) do
|
|
create(:closed_merge_request,
|
|
source_project: forked_project,
|
|
target_project: project)
|
|
end
|
|
|
|
it "returns false if the fork exist" do
|
|
expect(closed_merge_request.closed_without_fork?).to be_falsey
|
|
end
|
|
|
|
it "returns true if the fork does not exist" do
|
|
unlink_project.execute
|
|
closed_merge_request.reload
|
|
|
|
expect(closed_merge_request.closed_without_fork?).to be_truthy
|
|
end
|
|
end
|
|
|
|
context "when the merge request is open" do
|
|
let(:open_merge_request) do
|
|
create(:merge_request,
|
|
source_project: forked_project,
|
|
target_project: project)
|
|
end
|
|
|
|
it "returns false" do
|
|
expect(open_merge_request.closed_without_fork?).to be_falsey
|
|
end
|
|
end
|
|
end
|
|
|
|
describe '#reopenable?' do
|
|
context 'when the merge request is closed' do
|
|
it 'returns true' do
|
|
subject.close
|
|
|
|
expect(subject.reopenable?).to be_truthy
|
|
end
|
|
|
|
context 'forked project' do
|
|
let(:project) { create(:project, :public) }
|
|
let(:user) { create(:user) }
|
|
let(:forked_project) { fork_project(project, user) }
|
|
|
|
let!(:merge_request) do
|
|
create(:closed_merge_request,
|
|
source_project: forked_project,
|
|
target_project: project)
|
|
end
|
|
|
|
it 'returns false if unforked' do
|
|
Projects::UnlinkForkService.new(forked_project, user).execute
|
|
|
|
expect(merge_request.reload.reopenable?).to be_falsey
|
|
end
|
|
|
|
it 'returns false if the source project is deleted' do
|
|
Projects::DestroyService.new(forked_project, user).execute
|
|
|
|
expect(merge_request.reload.reopenable?).to be_falsey
|
|
end
|
|
|
|
it 'returns false if the merge request is merged' do
|
|
merge_request.update(state: 'merged')
|
|
|
|
expect(merge_request.reload.reopenable?).to be_falsey
|
|
end
|
|
end
|
|
end
|
|
|
|
context 'when the merge request is opened' do
|
|
it 'returns false' do
|
|
expect(subject.reopenable?).to be_falsey
|
|
end
|
|
end
|
|
end
|
|
|
|
describe '#mergeable_with_quick_action?' do
|
|
def create_pipeline(status)
|
|
pipeline = create(:ci_pipeline_with_one_job,
|
|
project: project,
|
|
ref: merge_request.source_branch,
|
|
sha: merge_request.diff_head_sha,
|
|
status: status,
|
|
head_pipeline_of: merge_request)
|
|
|
|
pipeline
|
|
end
|
|
|
|
let(:project) { create(:project, :public, :repository, only_allow_merge_if_pipeline_succeeds: true) }
|
|
let(:developer) { create(:user) }
|
|
let(:user) { create(:user) }
|
|
let(:merge_request) { create(:merge_request, source_project: project) }
|
|
let(:mr_sha) { merge_request.diff_head_sha }
|
|
|
|
before do
|
|
project.add_developer(developer)
|
|
end
|
|
|
|
context 'when autocomplete_precheck is set to true' do
|
|
it 'is mergeable by developer' do
|
|
expect(merge_request.mergeable_with_quick_action?(developer, autocomplete_precheck: true)).to be_truthy
|
|
end
|
|
|
|
it 'is not mergeable by normal user' do
|
|
expect(merge_request.mergeable_with_quick_action?(user, autocomplete_precheck: true)).to be_falsey
|
|
end
|
|
end
|
|
|
|
context 'when autocomplete_precheck is set to false' do
|
|
it 'is mergeable by developer' do
|
|
expect(merge_request.mergeable_with_quick_action?(developer, last_diff_sha: mr_sha)).to be_truthy
|
|
end
|
|
|
|
it 'is not mergeable by normal user' do
|
|
expect(merge_request.mergeable_with_quick_action?(user, last_diff_sha: mr_sha)).to be_falsey
|
|
end
|
|
|
|
context 'closed MR' do
|
|
before do
|
|
merge_request.update_attribute(:state, :closed)
|
|
end
|
|
|
|
it 'is not mergeable' do
|
|
expect(merge_request.mergeable_with_quick_action?(developer, last_diff_sha: mr_sha)).to be_falsey
|
|
end
|
|
end
|
|
|
|
context 'MR with WIP' do
|
|
before do
|
|
merge_request.update_attribute(:title, 'WIP: some MR')
|
|
end
|
|
|
|
it 'is not mergeable' do
|
|
expect(merge_request.mergeable_with_quick_action?(developer, last_diff_sha: mr_sha)).to be_falsey
|
|
end
|
|
end
|
|
|
|
context 'sha differs from the MR diff_head_sha' do
|
|
it 'is not mergeable' do
|
|
expect(merge_request.mergeable_with_quick_action?(developer, last_diff_sha: 'some other sha')).to be_falsey
|
|
end
|
|
end
|
|
|
|
context 'sha is not provided' do
|
|
it 'is not mergeable' do
|
|
expect(merge_request.mergeable_with_quick_action?(developer)).to be_falsey
|
|
end
|
|
end
|
|
|
|
context 'with pipeline ok' do
|
|
before do
|
|
create_pipeline(:success)
|
|
end
|
|
|
|
it 'is mergeable' do
|
|
expect(merge_request.mergeable_with_quick_action?(developer, last_diff_sha: mr_sha)).to be_truthy
|
|
end
|
|
end
|
|
|
|
context 'with failing pipeline' do
|
|
before do
|
|
create_pipeline(:failed)
|
|
end
|
|
|
|
it 'is not mergeable' do
|
|
expect(merge_request.mergeable_with_quick_action?(developer, last_diff_sha: mr_sha)).to be_falsey
|
|
end
|
|
end
|
|
|
|
context 'with running pipeline' do
|
|
before do
|
|
create_pipeline(:running)
|
|
end
|
|
|
|
it 'is mergeable' do
|
|
expect(merge_request.mergeable_with_quick_action?(developer, last_diff_sha: mr_sha)).to be_truthy
|
|
end
|
|
end
|
|
end
|
|
end
|
|
|
|
describe '#base_pipeline' do
|
|
let(:pipeline_arguments) do
|
|
{
|
|
project: project,
|
|
ref: merge_request.target_branch,
|
|
sha: merge_request.diff_base_sha
|
|
}
|
|
end
|
|
|
|
let(:project) { create(:project, :public, :repository) }
|
|
let(:merge_request) { create(:merge_request, source_project: project) }
|
|
|
|
let!(:first_pipeline) { create(:ci_pipeline_without_jobs, pipeline_arguments) }
|
|
let!(:last_pipeline) { create(:ci_pipeline_without_jobs, pipeline_arguments) }
|
|
let!(:last_pipeline_with_other_ref) { create(:ci_pipeline_without_jobs, pipeline_arguments.merge(ref: 'other')) }
|
|
|
|
it 'returns latest pipeline for the target branch' do
|
|
expect(merge_request.base_pipeline).to eq(last_pipeline)
|
|
end
|
|
end
|
|
|
|
describe '#has_commits?' do
|
|
it 'returns true when merge request diff has commits' do
|
|
allow(subject.merge_request_diff).to receive(:commits_count)
|
|
.and_return(2)
|
|
|
|
expect(subject.has_commits?).to be_truthy
|
|
end
|
|
|
|
context 'when commits_count is nil' do
|
|
it 'returns false' do
|
|
allow(subject.merge_request_diff).to receive(:commits_count)
|
|
.and_return(nil)
|
|
|
|
expect(subject.has_commits?).to be_falsey
|
|
end
|
|
end
|
|
end
|
|
|
|
describe '#has_no_commits?' do
|
|
before do
|
|
allow(subject.merge_request_diff).to receive(:commits_count)
|
|
.and_return(0)
|
|
end
|
|
|
|
it 'returns true when merge request diff has 0 commits' do
|
|
expect(subject.has_no_commits?).to be_truthy
|
|
end
|
|
end
|
|
|
|
describe '#merge_request_diff_for' do
|
|
subject { create(:merge_request, importing: true) }
|
|
let!(:merge_request_diff1) { subject.merge_request_diffs.create(head_commit_sha: '6f6d7e7ed97bb5f0054f2b1df789b39ca89b6ff9') }
|
|
let!(:merge_request_diff2) { subject.merge_request_diffs.create(head_commit_sha: nil) }
|
|
let!(:merge_request_diff3) { subject.merge_request_diffs.create(head_commit_sha: '5937ac0a7beb003549fc5fd26fc247adbce4a52e') }
|
|
|
|
context 'with diff refs' do
|
|
it 'returns the diffs' do
|
|
expect(subject.merge_request_diff_for(merge_request_diff1.diff_refs)).to eq(merge_request_diff1)
|
|
end
|
|
end
|
|
|
|
context 'with a commit SHA' do
|
|
it 'returns the diffs' do
|
|
expect(subject.merge_request_diff_for(merge_request_diff3.head_commit_sha)).to eq(merge_request_diff3)
|
|
end
|
|
end
|
|
|
|
it 'runs a single query on the initial call, and none afterwards' do
|
|
expect { subject.merge_request_diff_for(merge_request_diff1.diff_refs) }
|
|
.not_to exceed_query_limit(1)
|
|
|
|
expect { subject.merge_request_diff_for(merge_request_diff2.diff_refs) }
|
|
.not_to exceed_query_limit(0)
|
|
|
|
expect { subject.merge_request_diff_for(merge_request_diff3.head_commit_sha) }
|
|
.not_to exceed_query_limit(0)
|
|
end
|
|
end
|
|
|
|
describe '#version_params_for' do
|
|
subject { create(:merge_request, importing: true) }
|
|
let(:project) { subject.project }
|
|
let!(:merge_request_diff1) { subject.merge_request_diffs.create(head_commit_sha: '6f6d7e7ed97bb5f0054f2b1df789b39ca89b6ff9') }
|
|
let!(:merge_request_diff2) { subject.merge_request_diffs.create(head_commit_sha: nil) }
|
|
let!(:merge_request_diff3) { subject.merge_request_diffs.create(head_commit_sha: '5937ac0a7beb003549fc5fd26fc247adbce4a52e') }
|
|
|
|
context 'when the diff refs are for an older merge request version' do
|
|
let(:diff_refs) { merge_request_diff1.diff_refs }
|
|
|
|
it 'returns the diff ID for the version to show' do
|
|
expect(subject.version_params_for(diff_refs)).to eq(diff_id: merge_request_diff1.id)
|
|
end
|
|
end
|
|
|
|
context 'when the diff refs are for a comparison between merge request versions' do
|
|
let(:diff_refs) { merge_request_diff3.compare_with(merge_request_diff1.head_commit_sha).diff_refs }
|
|
|
|
it 'returns the diff ID and start sha of the versions to compare' do
|
|
expect(subject.version_params_for(diff_refs)).to eq(diff_id: merge_request_diff3.id, start_sha: merge_request_diff1.head_commit_sha)
|
|
end
|
|
end
|
|
|
|
context 'when the diff refs are not for a merge request version' do
|
|
let(:diff_refs) { project.commit(sample_commit.id).diff_refs }
|
|
|
|
it 'returns nil' do
|
|
expect(subject.version_params_for(diff_refs)).to be_nil
|
|
end
|
|
end
|
|
end
|
|
|
|
describe '#fetch_ref!' do
|
|
it 'fetches the ref correctly' do
|
|
expect { subject.target_project.repository.delete_refs(subject.ref_path) }.not_to raise_error
|
|
|
|
subject.fetch_ref!
|
|
expect(subject.target_project.repository.ref_exists?(subject.ref_path)).to be_truthy
|
|
end
|
|
end
|
|
|
|
describe 'removing a merge request' do
|
|
it 'refreshes the number of open merge requests of the target project' do
|
|
project = subject.target_project
|
|
|
|
expect { subject.destroy }
|
|
.to change { project.open_merge_requests_count }.from(1).to(0)
|
|
end
|
|
end
|
|
|
|
it_behaves_like 'throttled touch' do
|
|
subject { create(:merge_request, updated_at: 1.hour.ago) }
|
|
end
|
|
|
|
context 'state machine transitions' do
|
|
describe '#unlock_mr' do
|
|
subject { create(:merge_request, state: 'locked', merge_jid: 123) }
|
|
|
|
it 'updates merge request head pipeline and sets merge_jid to nil' do
|
|
pipeline = create(:ci_empty_pipeline, project: subject.project, ref: subject.source_branch, sha: subject.source_branch_sha)
|
|
|
|
subject.unlock_mr
|
|
|
|
subject.reload
|
|
expect(subject.head_pipeline).to eq(pipeline)
|
|
expect(subject.merge_jid).to be_nil
|
|
end
|
|
end
|
|
|
|
describe 'transition to cannot_be_merged' do
|
|
let(:notification_service) { double(:notification_service) }
|
|
let(:todo_service) { double(:todo_service) }
|
|
subject { create(:merge_request, state, merge_status: :unchecked) }
|
|
|
|
before do
|
|
allow(NotificationService).to receive(:new).and_return(notification_service)
|
|
allow(TodoService).to receive(:new).and_return(todo_service)
|
|
|
|
allow(subject.project.repository).to receive(:can_be_merged?).and_return(false)
|
|
end
|
|
|
|
[:opened, :locked].each do |state|
|
|
context state do
|
|
let(:state) { state }
|
|
|
|
it 'notifies conflict, but does not notify again if rechecking still results in cannot_be_merged' do
|
|
expect(notification_service).to receive(:merge_request_unmergeable).with(subject).once
|
|
expect(todo_service).to receive(:merge_request_became_unmergeable).with(subject).once
|
|
|
|
subject.mark_as_unmergeable
|
|
subject.mark_as_unchecked
|
|
subject.mark_as_unmergeable
|
|
end
|
|
|
|
it 'notifies conflict, whenever newly unmergeable' do
|
|
expect(notification_service).to receive(:merge_request_unmergeable).with(subject).twice
|
|
expect(todo_service).to receive(:merge_request_became_unmergeable).with(subject).twice
|
|
|
|
subject.mark_as_unmergeable
|
|
subject.mark_as_unchecked
|
|
subject.mark_as_mergeable
|
|
subject.mark_as_unchecked
|
|
subject.mark_as_unmergeable
|
|
end
|
|
|
|
it 'does not notify whenever merge request is newly unmergeable due to other reasons' do
|
|
allow(subject.project.repository).to receive(:can_be_merged?).and_return(true)
|
|
|
|
expect(notification_service).not_to receive(:merge_request_unmergeable)
|
|
expect(todo_service).not_to receive(:merge_request_became_unmergeable)
|
|
|
|
subject.mark_as_unmergeable
|
|
end
|
|
end
|
|
end
|
|
|
|
[:closed, :merged].each do |state|
|
|
let(:state) { state }
|
|
|
|
context state do
|
|
it 'does not notify' do
|
|
expect(notification_service).not_to receive(:merge_request_unmergeable)
|
|
expect(todo_service).not_to receive(:merge_request_became_unmergeable)
|
|
|
|
subject.mark_as_unmergeable
|
|
end
|
|
end
|
|
end
|
|
|
|
context 'source branch is missing' do
|
|
subject { create(:merge_request, :invalid, :opened, merge_status: :unchecked, target_branch: 'master') }
|
|
|
|
before do
|
|
allow(subject.project.repository).to receive(:can_be_merged?).and_call_original
|
|
end
|
|
|
|
it 'does not raise error' do
|
|
expect(notification_service).not_to receive(:merge_request_unmergeable)
|
|
expect(todo_service).not_to receive(:merge_request_became_unmergeable)
|
|
|
|
expect { subject.mark_as_unmergeable }.not_to raise_error
|
|
expect(subject.cannot_be_merged?).to eq(true)
|
|
end
|
|
end
|
|
end
|
|
|
|
describe 'check_state?' do
|
|
it 'indicates whether MR is still checking for mergeability' do
|
|
state_machine = described_class.state_machines[:merge_status]
|
|
check_states = [:unchecked, :cannot_be_merged_recheck]
|
|
|
|
check_states.each do |merge_status|
|
|
expect(state_machine.check_state?(merge_status)).to be true
|
|
end
|
|
|
|
(state_machine.states.map(&:name) - check_states).each do |merge_status|
|
|
expect(state_machine.check_state?(merge_status)).to be false
|
|
end
|
|
end
|
|
end
|
|
end
|
|
|
|
describe '#should_be_rebased?' do
|
|
let(:project) { create(:project, :repository) }
|
|
|
|
it 'returns false for the same source and target branches' do
|
|
merge_request = create(:merge_request, source_project: project, target_project: project)
|
|
|
|
expect(merge_request.should_be_rebased?).to be_falsey
|
|
end
|
|
end
|
|
|
|
describe '#rebase_in_progress?' do
|
|
where(:rebase_jid, :jid_valid, :result) do
|
|
'foo' | true | true
|
|
'foo' | false | false
|
|
'' | true | false
|
|
nil | true | false
|
|
end
|
|
|
|
with_them do
|
|
let(:merge_request) { create(:merge_request) }
|
|
|
|
subject { merge_request.rebase_in_progress? }
|
|
|
|
it do
|
|
# Stub out the legacy gitaly implementation
|
|
allow(merge_request).to receive(:gitaly_rebase_in_progress?) { false }
|
|
|
|
allow(Gitlab::SidekiqStatus).to receive(:running?).with(rebase_jid) { jid_valid }
|
|
|
|
merge_request.rebase_jid = rebase_jid
|
|
|
|
is_expected.to eq(result)
|
|
end
|
|
end
|
|
end
|
|
|
|
describe '#gitaly_rebase_in_progress?' do
|
|
let(:repo_path) do
|
|
Gitlab::GitalyClient::StorageSettings.allow_disk_access do
|
|
subject.source_project.repository.path
|
|
end
|
|
end
|
|
let(:rebase_path) { File.join(repo_path, "gitlab-worktree", "rebase-#{subject.id}") }
|
|
|
|
before do
|
|
system(*%W(#{Gitlab.config.git.bin_path} -C #{repo_path} worktree add --detach #{rebase_path} master))
|
|
end
|
|
|
|
it 'returns true when there is a current rebase directory' do
|
|
expect(subject.rebase_in_progress?).to be_truthy
|
|
end
|
|
|
|
it 'returns false when there is no rebase directory' do
|
|
FileUtils.rm_rf(rebase_path)
|
|
|
|
expect(subject.rebase_in_progress?).to be_falsey
|
|
end
|
|
|
|
it 'returns false when the rebase directory has expired' do
|
|
time = 20.minutes.ago.to_time
|
|
File.utime(time, time, rebase_path)
|
|
|
|
expect(subject.rebase_in_progress?).to be_falsey
|
|
end
|
|
|
|
it 'returns false when the source project has been removed' do
|
|
allow(subject).to receive(:source_project).and_return(nil)
|
|
|
|
expect(subject.rebase_in_progress?).to be_falsey
|
|
end
|
|
end
|
|
|
|
describe '#allow_collaboration' do
|
|
let(:merge_request) do
|
|
build(:merge_request, source_branch: 'fixes', allow_collaboration: true)
|
|
end
|
|
|
|
it 'is false when pushing by a maintainer is not possible' do
|
|
expect(merge_request).to receive(:collaborative_push_possible?) { false }
|
|
|
|
expect(merge_request.allow_collaboration).to be_falsy
|
|
end
|
|
|
|
it 'is true when pushing by a maintainer is possible' do
|
|
expect(merge_request).to receive(:collaborative_push_possible?) { true }
|
|
|
|
expect(merge_request.allow_collaboration).to be_truthy
|
|
end
|
|
end
|
|
|
|
describe '#collaborative_push_possible?' do
|
|
let(:merge_request) do
|
|
build(:merge_request, source_branch: 'fixes')
|
|
end
|
|
|
|
before do
|
|
allow(ProtectedBranch).to receive(:protected?) { false }
|
|
end
|
|
|
|
it 'does not allow maintainer to push if the source project is the same as the target' do
|
|
merge_request.target_project = merge_request.source_project = create(:project, :public)
|
|
|
|
expect(merge_request.collaborative_push_possible?).to be_falsy
|
|
end
|
|
|
|
it 'allows maintainer to push when both source and target are public' do
|
|
merge_request.target_project = build(:project, :public)
|
|
merge_request.source_project = build(:project, :public)
|
|
|
|
expect(merge_request.collaborative_push_possible?).to be_truthy
|
|
end
|
|
|
|
it 'is not available for protected branches' do
|
|
merge_request.target_project = build(:project, :public)
|
|
merge_request.source_project = build(:project, :public)
|
|
|
|
expect(ProtectedBranch).to receive(:protected?)
|
|
.with(merge_request.source_project, 'fixes')
|
|
.and_return(true)
|
|
|
|
expect(merge_request.collaborative_push_possible?).to be_falsy
|
|
end
|
|
end
|
|
|
|
describe '#includes_any_commits?' do
|
|
it 'returns false' do
|
|
expect(subject.includes_any_commits?([])).to be_falsey
|
|
end
|
|
|
|
it 'returns false' do
|
|
expect(subject.includes_any_commits?([Gitlab::Git::BLANK_SHA])).to be_falsey
|
|
end
|
|
|
|
it 'returns true' do
|
|
expect(subject.includes_any_commits?([subject.merge_request_diff.head_commit_sha])).to be_truthy
|
|
end
|
|
|
|
it 'returns true even when there is a non-existent comit' do
|
|
expect(subject.includes_any_commits?([Gitlab::Git::BLANK_SHA, subject.merge_request_diff.head_commit_sha])).to be_truthy
|
|
end
|
|
|
|
context 'unpersisted merge request' do
|
|
let(:new_mr) { build(:merge_request) }
|
|
|
|
it 'returns false' do
|
|
expect(new_mr.includes_any_commits?([Gitlab::Git::BLANK_SHA])).to be_falsey
|
|
end
|
|
|
|
it 'returns true' do
|
|
expect(new_mr.includes_any_commits?([subject.merge_request_diff.head_commit_sha])).to be_truthy
|
|
end
|
|
end
|
|
end
|
|
|
|
describe '#can_allow_collaboration?' do
|
|
let(:target_project) { create(:project, :public) }
|
|
let(:source_project) { fork_project(target_project) }
|
|
let(:merge_request) do
|
|
create(:merge_request,
|
|
source_project: source_project,
|
|
source_branch: 'fixes',
|
|
target_project: target_project)
|
|
end
|
|
let(:user) { create(:user) }
|
|
|
|
before do
|
|
allow(merge_request).to receive(:collaborative_push_possible?) { true }
|
|
end
|
|
|
|
it 'is false if the user does not have push access to the source project' do
|
|
expect(merge_request.can_allow_collaboration?(user)).to be_falsy
|
|
end
|
|
|
|
it 'is true when the user has push access to the source project' do
|
|
source_project.add_developer(user)
|
|
|
|
expect(merge_request.can_allow_collaboration?(user)).to be_truthy
|
|
end
|
|
end
|
|
|
|
describe '#merge_participants' do
|
|
it 'contains author' do
|
|
expect(subject.merge_participants).to eq([subject.author])
|
|
end
|
|
|
|
describe 'when merge_when_pipeline_succeeds? is true' do
|
|
describe 'when merge user is author' do
|
|
let(:user) { create(:user) }
|
|
subject do
|
|
create(:merge_request,
|
|
merge_when_pipeline_succeeds: true,
|
|
merge_user: user,
|
|
author: user)
|
|
end
|
|
|
|
it 'contains author only' do
|
|
expect(subject.merge_participants).to eq([subject.author])
|
|
end
|
|
end
|
|
|
|
describe 'when merge user and author are different users' do
|
|
let(:merge_user) { create(:user) }
|
|
subject do
|
|
create(:merge_request,
|
|
merge_when_pipeline_succeeds: true,
|
|
merge_user: merge_user)
|
|
end
|
|
|
|
it 'contains author and merge user' do
|
|
expect(subject.merge_participants).to eq([subject.author, merge_user])
|
|
end
|
|
end
|
|
end
|
|
end
|
|
|
|
describe '.merge_request_ref?' do
|
|
subject { described_class.merge_request_ref?(ref) }
|
|
|
|
context 'when ref is ref name of a branch' do
|
|
let(:ref) { 'feature' }
|
|
|
|
it { is_expected.to be_falsey }
|
|
end
|
|
|
|
context 'when ref is HEAD ref path of a branch' do
|
|
let(:ref) { 'refs/heads/feature' }
|
|
|
|
it { is_expected.to be_falsey }
|
|
end
|
|
|
|
context 'when ref is HEAD ref path of a merge request' do
|
|
let(:ref) { 'refs/merge-requests/1/head' }
|
|
|
|
it { is_expected.to be_truthy }
|
|
end
|
|
|
|
context 'when ref is merge ref path of a merge request' do
|
|
let(:ref) { 'refs/merge-requests/1/merge' }
|
|
|
|
it { is_expected.to be_truthy }
|
|
end
|
|
end
|
|
|
|
describe '#cleanup_refs' do
|
|
subject { merge_request.cleanup_refs(only: only) }
|
|
|
|
let(:merge_request) { build(:merge_request) }
|
|
|
|
context 'when removing all refs' do
|
|
let(:only) { :all }
|
|
|
|
it 'deletes all refs from the target project' do
|
|
expect(merge_request.target_project.repository)
|
|
.to receive(:delete_refs)
|
|
.with(merge_request.ref_path, merge_request.merge_ref_path, merge_request.train_ref_path)
|
|
|
|
subject
|
|
end
|
|
end
|
|
|
|
context 'when removing only train ref' do
|
|
let(:only) { :train }
|
|
|
|
it 'deletes train ref from the target project' do
|
|
expect(merge_request.target_project.repository)
|
|
.to receive(:delete_refs)
|
|
.with(merge_request.train_ref_path)
|
|
|
|
subject
|
|
end
|
|
end
|
|
end
|
|
end
|