summaryrefslogtreecommitdiff
path: root/app/services/git/branch_hooks_service.rb
blob: 93a0d139001bd5d241daca764ca356992e6b6ebe (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
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
# frozen_string_literal: true

module Git
  class BranchHooksService < ::Git::BaseHooksService
    def execute
      execute_branch_hooks

      super.tap do
        enqueue_update_signatures
      end
    end

    private

    def hook_name
      :push_hooks
    end

    def commits
      strong_memoize(:commits) do
        if creating_default_branch?
          # The most recent PROCESS_COMMIT_LIMIT commits in the default branch
          project.repository.commits(newrev, limit: PROCESS_COMMIT_LIMIT)
        elsif creating_branch?
          # Use the pushed commits that aren't reachable by the default branch
          # as a heuristic. This may include more commits than are actually
          # pushed, but that shouldn't matter because we check for existing
          # cross-references later.
          project.repository.commits_between(project.default_branch, newrev)
        elsif updating_branch?
          project.repository.commits_between(oldrev, newrev)
        else # removing branch
          []
        end
      end
    end

    def commits_count
      return count_commits_in_branch if creating_default_branch?

      super
    end

    def invalidated_file_types
      return super unless default_branch? && !creating_branch?

      paths = limited_commits.each_with_object(Set.new) do |commit, set|
        commit.raw_deltas.each do |diff|
          set << diff.new_path
        end
      end

      Gitlab::FileDetector.types_in_paths(paths)
    end

    def execute_branch_hooks
      project.repository.after_push_commit(branch_name)

      branch_create_hooks if creating_branch?
      branch_update_hooks if updating_branch?
      branch_change_hooks if creating_branch? || updating_branch?
      branch_remove_hooks if removing_branch?
    end

    def branch_create_hooks
      project.repository.after_create_branch(expire_cache: false)
      project.after_create_default_branch if default_branch?
    end

    def branch_update_hooks
      # Update the bare repositories info/attributes file using the contents of
      # the default branch's .gitattributes file
      project.repository.copy_gitattributes(ref) if default_branch?
    end

    def branch_change_hooks
      enqueue_process_commit_messages
      enqueue_jira_connect_sync_messages
      enqueue_metrics_dashboard_sync
    end

    def branch_remove_hooks
      project.repository.after_remove_branch(expire_cache: false)
    end

    def enqueue_metrics_dashboard_sync
      return unless Feature.enabled?(:sync_metrics_dashboards, project)
      return unless default_branch?

      ::Metrics::Dashboard::SyncDashboardsWorker.perform_async(project.id)
    end

    # Schedules processing of commit messages
    def enqueue_process_commit_messages
      referencing_commits = limited_commits.select(&:matches_cross_reference_regex?)

      upstream_commit_ids = upstream_commit_ids(referencing_commits)

      referencing_commits.each do |commit|
        # Avoid reprocessing commits that already exist upstream if the project
        # is a fork. This will prevent duplicated/superfluous system notes on
        # mentionables referenced by a commit that is pushed to the upstream,
        # that is then also pushed to forks when these get synced by users.
        next if upstream_commit_ids.include?(commit.id)

        ProcessCommitWorker.perform_async(
          project.id,
          current_user.id,
          commit.to_hash,
          default_branch?
        )
      end
    end

    def enqueue_jira_connect_sync_messages
      return unless project.jira_subscription_exists?

      branch_to_sync = branch_name if Atlassian::JiraIssueKeyExtractor.has_keys?(branch_name)
      commits_to_sync = limited_commits.select { |commit| Atlassian::JiraIssueKeyExtractor.has_keys?(commit.safe_message) }.map(&:sha)

      if branch_to_sync || commits_to_sync.any?
        JiraConnect::SyncBranchWorker.perform_async(project.id, branch_to_sync, commits_to_sync)
      end
    end

    def unsigned_x509_shas(commits)
      X509CommitSignature.unsigned_commit_shas(commits.map(&:sha))
    end

    def unsigned_gpg_shas(commits)
      GpgSignature.unsigned_commit_shas(commits.map(&:sha))
    end

    def enqueue_update_signatures
      unsigned = unsigned_x509_shas(limited_commits) & unsigned_gpg_shas(limited_commits)
      return if unsigned.empty?

      signable = Gitlab::Git::Commit.shas_with_signatures(project.repository, unsigned)
      return if signable.empty?

      CreateCommitSignatureWorker.perform_async(signable, project.id)
    end

    # It's not sufficient to just check for a blank SHA as it's possible for the
    # branch to be pushed, but for the `post-receive` hook to never run:
    # https://gitlab.com/gitlab-org/gitlab-foss/issues/59257
    def creating_branch?
      strong_memoize(:creating_branch) do
        Gitlab::Git.blank_ref?(oldrev) ||
          !project.repository.branch_exists?(branch_name)
      end
    end

    def updating_branch?
      !creating_branch? && !removing_branch?
    end

    def removing_branch?
      Gitlab::Git.blank_ref?(newrev)
    end

    def creating_default_branch?
      creating_branch? && default_branch?
    end

    def count_commits_in_branch
      strong_memoize(:count_commits_in_branch) do
        project.repository.commit_count_for_ref(ref)
      end
    end

    def default_branch?
      strong_memoize(:default_branch) do
        [nil, branch_name].include?(project.default_branch)
      end
    end

    def branch_name
      strong_memoize(:branch_name) { Gitlab::Git.ref_name(ref) }
    end

    def upstream_commit_ids(commits)
      set = Set.new

      upstream_project = project.fork_source
      if upstream_project
        upstream_project
          .commits_by(oids: commits.map(&:id))
          .each { |commit| set << commit.id }
      end

      set
    end
  end
end

Git::BranchHooksService.prepend_if_ee('::EE::Git::BranchHooksService')