From 9e126a06392558f8f4787cfd55c1bc0f34992c62 Mon Sep 17 00:00:00 2001
From: Hordur Freyr Yngvason <hfyngvason@gitlab.com>
Date: Tue, 11 Mar 2025 07:09:05 -0400
Subject: [PATCH] Fix merge request pipelines on ambiguous refs

When the merge request source branch was ambiguous in the source
project, detached merge request pipelines would pick commit SHA
associated with the tag instead of the merge request's branch.

We fix that by using `source_branch_ref` instead of `source_branch` in
the places deciding the commit used by detached merge request pipelines.

Changelog: fixed
---
 app/models/merge_request.rb                   |  6 +--
 .../merge_requests/create_pipeline_service.rb |  2 +-
 spec/models/merge_request_spec.rb             | 54 ++++++++++++++++---
 .../create_pipeline_service_spec.rb           | 26 +++++++++
 4 files changed, 77 insertions(+), 11 deletions(-)

diff --git a/app/models/merge_request.rb b/app/models/merge_request.rb
index cfea6a5559278..a5ff789d77fa2 100644
--- a/app/models/merge_request.rb
+++ b/app/models/merge_request.rb
@@ -1035,8 +1035,8 @@ def diff_head_sha
   # We use these attributes to force these to the intended values.
   attr_writer :target_branch_sha, :source_branch_sha
 
-  def source_branch_ref
-    return @source_branch_sha if @source_branch_sha
+  def source_branch_ref(or_sha: true)
+    return @source_branch_sha if @source_branch_sha && or_sha
     return unless source_branch
 
     Gitlab::Git::BRANCH_REF_PREFIX + source_branch
@@ -1754,7 +1754,7 @@ def environments_in_head_pipeline(deployment_status: nil)
   end
 
   def fetch_ref!
-    target_project.repository.fetch_source_branch!(source_project.repository, source_branch, ref_path)
+    target_project.repository.fetch_source_branch!(source_project.repository, source_branch_ref(or_sha: false), ref_path)
     expire_ancestor_cache
   end
 
diff --git a/app/services/merge_requests/create_pipeline_service.rb b/app/services/merge_requests/create_pipeline_service.rb
index 0e245d47bd309..cea861aa28c45 100644
--- a/app/services/merge_requests/create_pipeline_service.rb
+++ b/app/services/merge_requests/create_pipeline_service.rb
@@ -52,7 +52,7 @@ def detached_pipeline_project_and_ref(merge_request)
       if can_create_pipeline_in_target_project?(merge_request)
         [merge_request.target_project, merge_request.ref_path]
       else
-        [merge_request.source_project, merge_request.source_branch]
+        [merge_request.source_project, merge_request.source_branch_ref(or_sha: false)]
       end
     end
 
diff --git a/spec/models/merge_request_spec.rb b/spec/models/merge_request_spec.rb
index 1a0b5692add9f..dc20531722c35 100644
--- a/spec/models/merge_request_spec.rb
+++ b/spec/models/merge_request_spec.rb
@@ -1489,6 +1489,29 @@ def expected_total_time(mrs)
     end
   end
 
+  describe '#source_branch_ref' do
+    using RSpec::Parameterized::TableSyntax
+
+    where(:or_sha, :source_branch, :source_branch_sha, :expected) do
+      true | nil | 'sha' | 'sha'
+      true | 'branch' | 'sha' | 'sha'
+      true | 'branch' | nil | 'refs/heads/branch'
+      false | nil | 'sha' | nil
+      false | 'branch' | 'sha' | 'refs/heads/branch'
+      false | 'branch' | 'sha' | 'refs/heads/branch'
+    end
+
+    with_them do
+      specify do
+        merge_request = build(:merge_request, source_branch:)
+        merge_request.source_branch_sha = source_branch_sha
+        result = merge_request.source_branch_ref(or_sha:)
+
+        expect(result).to eq(expected)
+      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) }
 
@@ -5009,19 +5032,36 @@ def create_build(pipeline, coverage, name)
   end
 
   describe '#fetch_ref!' do
-    let(:project) { create(:project, :repository) }
+    let_it_be(:project) { create(:project, :repository) }
+    let(:merge_request) { create(:merge_request, source_project: project) }
 
-    subject { create(:merge_request, source_project: project) }
+    before_all do
+      repository = project.repository
+      repository.add_branch(project.creator, 'ambiguous', 'feature')
+      repository.add_tag(project.creator, 'ambiguous', 'master')
+    end
 
     it 'fetches the ref and expires the ancestor cache' do
-      expect { subject.target_project.repository.delete_refs(subject.ref_path) }.not_to raise_error
+      expect { merge_request.target_project.repository.delete_refs(merge_request.ref_path) }.not_to raise_error
 
-      expect(project.repository).to receive(:expire_ancestor_cache).with(subject.target_branch_sha, subject.diff_head_sha).and_call_original
-      expect(subject).to receive(:expire_ancestor_cache).and_call_original
+      expect(project.repository).to receive(:expire_ancestor_cache).with(merge_request.target_branch_sha, merge_request.diff_head_sha).and_call_original
+      expect(merge_request).to receive(:expire_ancestor_cache).and_call_original
 
-      subject.fetch_ref!
+      merge_request.fetch_ref!
 
-      expect(subject.target_project.repository.ref_exists?(subject.ref_path)).to be_truthy
+      expect(merge_request.target_project.repository.ref_exists?(merge_request.ref_path)).to be_truthy
+    end
+
+    context 'when source branch is ambiguous' do
+      let(:merge_request) { create(:merge_request, source_project: project, source_branch: 'ambiguous') }
+
+      it 'fetches the branch, not the tag' do
+        merge_request.fetch_ref!
+
+        ref_path_sha = merge_request.target_project.commit(merge_request.ref_path).sha
+        expect(ref_path_sha).to eq(merge_request.source_branch_sha)
+        expect(ref_path_sha).not_to eq(project.commit('refs/tags/ambiguous').sha)
+      end
     end
   end
 
diff --git a/spec/services/merge_requests/create_pipeline_service_spec.rb b/spec/services/merge_requests/create_pipeline_service_spec.rb
index ecd233cf0c345..b68412962bd56 100644
--- a/spec/services/merge_requests/create_pipeline_service_spec.rb
+++ b/spec/services/merge_requests/create_pipeline_service_spec.rb
@@ -47,6 +47,32 @@
       expect(response.payload).to be_detached_merge_request_pipeline
     end
 
+    context 'when ref is ambiguous' do
+      let(:merge_request) do
+        create(:merge_request,
+          source_branch: 'ambiguous',
+          source_project: source_project,
+          target_branch: 'master',
+          target_project: project)
+      end
+
+      before do
+        repository = source_project.repository
+        repository.add_branch(user, 'ambiguous', 'feature')
+        repository.add_tag(user, 'ambiguous', 'master')
+      end
+
+      it 'creates a detached merge request pipeline for the branch, not the tag', :aggregate_failures do
+        expect { response }.to change { Ci::Pipeline.count }.by(1)
+
+        expect(response).to be_success
+        expect(response.payload).to be_persisted
+        expect(response.payload).to be_detached_merge_request_pipeline
+        expect(response.payload.sha).to eq merge_request.source_branch_sha
+        expect(response.payload.sha).not_to eq(source_project.commit('refs/tags/ambiguous').sha)
+      end
+    end
+
     it 'defaults to merge_request_event' do
       expect(response.payload.source).to eq('merge_request_event')
     end
-- 
GitLab