Welcome to mirror list, hosted at ThFree Co, Russian Federation.

branch_push_merge_commit_analyzer.rb « gitlab « lib - gitlab.com/gitlab-org/gitlab-foss.git - Unnamed repository; edit this file 'description' to name the repository.
summaryrefslogtreecommitdiff
blob: 046e83b0cf131ba06e8d5b70da670180f8120bc6 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
# frozen_string_literal: true

module Gitlab
  # Analyse a graph of commits from a push to a branch,
  # for each commit, analyze that if it is the head of a merge request,
  # then what should its merge_commit be, relative to the branch.
  #
  # A----->B----->C----->D   target branch
  # |             ^
  # |             |
  # +-->E----->F--+          merged branch
  #     |     ^
  #     |     |
  #     +->G--+
  #
  # (See merge-commit-analyze-after branch in gitlab-test)
  #
  # Assuming
  # - A is already in remote
  # - B~D are all in its own branch with its own merge request, targeting the target branch
  #
  # When D is finally pushed to the target branch,
  # what are the merge commits for all the other merge requests?
  #
  # We can walk backwards from the HEAD commit D,
  # and find status of its parents.
  # First we determine if commit belongs to the target branch (i.e. A, B, C, D),
  # and then determine its merge commit.
  #
  # +--------+-----------------+--------------+
  # | Commit | Direct ancestor | Merge commit |
  # +--------+-----------------+--------------+
  # | D      | Y               | D            |
  # +--------+-----------------+--------------+
  # | C      | Y               | C            |
  # +--------+-----------------+--------------+
  # | F      |                 | C            |
  # +--------+-----------------+--------------+
  # | B      | Y               | B            |
  # +--------+-----------------+--------------+
  # | E      |                 | C            |
  # +--------+-----------------+--------------+
  # | G      |                 | C            |
  # +--------+-----------------+--------------+
  #
  # By examining the result, it can be said that
  #
  # - If commit is direct ancestor of HEAD, its merge commit is itself.
  # - Otherwise, the merge commit is the same as its child's merge commit.
  #
  class BranchPushMergeCommitAnalyzer
    class CommitDecorator < SimpleDelegator
      attr_accessor :merge_commit
      attr_writer :direct_ancestor # boolean

      def direct_ancestor?
        @direct_ancestor
      end

      # @param child_commit [CommitDecorator]
      # @param first_parent [Boolean] whether `self` is the first parent of `child_commit`
      def set_merge_commit(child_commit, first_parent:)
        # If child commit is a direct ancestor, its first parent is also the direct ancestor.
        # We assume direct ancestors matches the trail of the target branch over time,
        # This assumption is correct most of the time, especially for gitlab managed merges,
        # but there are exception cases which can't be solved (https://stackoverflow.com/a/49754723/474597)
        @direct_ancestor = first_parent && child_commit.direct_ancestor?

        @merge_commit = direct_ancestor? ? self : child_commit.merge_commit
      end
    end

    # @param commits [Array] list of commits, must be ordered from the child (tip) of the graph back to the ancestors
    def initialize(commits, relevant_commit_ids: nil)
      @commits = commits
      @id_to_commit = {}
      @commits.each do |commit|
        @id_to_commit[commit.id] = CommitDecorator.new(commit)

        if relevant_commit_ids
          relevant_commit_ids.delete(commit.id)
          break if relevant_commit_ids.empty? # Only limit the analyze up to relevant_commit_ids
        end
      end

      analyze
    end

    def get_merge_commit(id)
      get_commit(id).merge_commit.id
    end

    private

    def analyze
      head_commit = get_commit(@commits.first.id)
      head_commit.direct_ancestor = true
      head_commit.merge_commit = head_commit

      # Analyzing a commit requires its child commit be analyzed first,
      # which is the case here since commits are ordered from child to parent.
      @id_to_commit.each_value do |commit|
        analyze_parents(commit)
      end
    end

    def analyze_parents(commit)
      commit.parent_ids.each.with_index do |parent_commit_id, i|
        parent_commit = get_commit(parent_commit_id)

        next if parent_commit.nil? # parent commit may not be part of new commits

        parent_commit.set_merge_commit(commit, first_parent: i == 0)
      end
    end

    def get_commit(id)
      @id_to_commit[id]
    end
  end
end