1# frozen_string_literal: true
2
3module Gitlab
4  module Git
5    class CrossRepoComparer
6      attr_reader :source_repo, :target_repo
7
8      def initialize(source_repo, target_repo)
9        @source_repo = source_repo
10        @target_repo = target_repo
11      end
12
13      def compare(source_ref, target_ref, straight:)
14        ensuring_ref_in_source(target_ref) do |target_commit_id|
15          Gitlab::Git::Compare.new(
16            source_repo,
17            target_commit_id,
18            source_ref,
19            straight: straight
20          )
21        end
22      end
23
24      private
25
26      def ensuring_ref_in_source(ref, &blk)
27        return yield ref if source_repo == target_repo
28
29        # If the commit doesn't exist in the target, there's nothing we can do
30        commit_id = target_repo.commit(ref)&.sha
31        return unless commit_id
32
33        # The commit pointed to by ref may exist in the source even when they
34        # are different repositories. This is particularly true of close forks,
35        # but may also be the case if a temporary ref for this comparison has
36        # already been created in the past, and the result hasn't been GC'd yet.
37        return yield commit_id if source_repo.commit(commit_id)
38
39        # Worst case: the commit is not in the source repo so we need to fetch
40        # it. Use a temporary ref and clean up afterwards
41        with_commit_in_source_tmp(commit_id, &blk)
42      end
43
44      # Fetch the ref into source_repo from target_repo, using a temporary ref
45      # name that will be deleted once the method completes. This is a no-op if
46      # fetching the source branch fails
47      def with_commit_in_source_tmp(commit_id, &blk)
48        tmp_ref = "refs/tmp/#{SecureRandom.hex}"
49
50        yield commit_id if source_repo.fetch_source_branch!(target_repo, commit_id, tmp_ref)
51      ensure
52        source_repo.delete_refs(tmp_ref) # best-effort
53      end
54    end
55  end
56end
57