summaryrefslogtreecommitdiff
path: root/app/services/git/base_hooks_service.rb
blob: 63f3f73905afe2bf81e6ffa90e93d2c4c8220abd (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
# frozen_string_literal: true

module Git
  class BaseHooksService < ::BaseService
    include Gitlab::Utils::StrongMemoize
    include ChangeParams

    # The N most recent commits to process in a single push payload.
    PROCESS_COMMIT_LIMIT = 100

    def execute
      create_events
      create_pipelines
      execute_project_hooks

      # Not a hook, but it needs access to the list of changed commits
      enqueue_invalidate_cache

      success
    end

    private

    def hook_name
      raise NotImplementedError, "Please implement #{self.class}##{__method__}"
    end

    # This should return PROCESS_COMMIT_LIMIT commits, ordered with newest last
    def limited_commits
      raise NotImplementedError, "Please implement #{self.class}##{__method__}"
    end

    def commits_count
      raise NotImplementedError, "Please implement #{self.class}##{__method__}"
    end

    def event_message
      nil
    end

    def invalidated_file_types
      []
    end

    # Push events in the activity feed only show information for the
    # last commit.
    def create_events
      return unless params.fetch(:create_push_event, true)

      EventCreateService.new.push(project, current_user, event_push_data)
    end

    def create_pipelines
      return unless params.fetch(:create_pipelines, true)

      Ci::CreatePipelineService
        .new(project, current_user, pipeline_params)
        .execute!(:push, pipeline_options)
    rescue Ci::CreatePipelineService::CreateError => ex
      log_pipeline_errors(ex)
    end

    def execute_project_hooks
      return unless params.fetch(:execute_project_hooks, true)

      # Creating push_data invokes one CommitDelta RPC per commit. Only
      # build this data if we actually need it.
      project.execute_hooks(push_data, hook_name) if project.has_active_hooks?(hook_name)
      project.execute_integrations(push_data, hook_name) if project.has_active_integrations?(hook_name)
    end

    def enqueue_invalidate_cache
      file_types = invalidated_file_types

      return unless file_types.present?

      ProjectCacheWorker.perform_async(project.id, file_types, [], false)
    end

    def pipeline_params
      strong_memoize(:pipeline_params) do
        {
          before: oldrev,
          after: newrev,
          ref: ref,
          variables_attributes: generate_vars_from_push_options || [],
          push_options: params[:push_options] || {},
          checkout_sha: Gitlab::DataBuilder::Push.checkout_sha(
            project.repository, newrev, ref)
        }
      end
    end

    def ci_variables_from_push_options
      strong_memoize(:ci_variables_from_push_options) do
        params[:push_options]&.deep_symbolize_keys&.dig(:ci, :variable)
      end
    end

    def generate_vars_from_push_options
      return [] unless ci_variables_from_push_options

      ci_variables_from_push_options.map do |var_definition, _count|
        key, value = var_definition.to_s.split("=", 2)

        # Accept only valid format. We ignore the following formats
        # 1. "=123". In this case, `key` will be an empty string
        # 2. "FOO". In this case, `value` will be nil.
        # However, the format "FOO=" will result in key beign `FOO` and value
        # being an empty string. This is acceptable.
        next if key.blank? || value.nil?

        { "key" => key, "variable_type" => "env_var", "secret_value" => value }
      end.compact
    end

    def push_data_params(commits:, with_changed_files: true)
      {
        oldrev: oldrev,
        newrev: newrev,
        ref: ref,
        project: project,
        user: current_user,
        commits: commits,
        message: event_message,
        commits_count: commits_count,
        with_changed_files: with_changed_files
      }
    end

    def event_push_data
      # We only need the newest commit for the event push, and we don't
      # need the full deltas either.
      @event_push_data ||= Gitlab::DataBuilder::Push.build(
        **push_data_params(commits: limited_commits.last, with_changed_files: false)
      )
    end

    def push_data
      @push_data ||= Gitlab::DataBuilder::Push.build(**push_data_params(commits: limited_commits))

      # Dependent code may modify the push data, so return a duplicate each time
      @push_data.dup
    end

    # to be overridden in EE
    def pipeline_options
      {}
    end

    def log_pipeline_errors(exception)
      data = {
        class: self.class.name,
        correlation_id: Labkit::Correlation::CorrelationId.current_id.to_s,
        project_id: project.id,
        project_path: project.full_path,
        message: "Error creating pipeline",
        errors: exception.to_s,
        pipeline_params: sanitized_pipeline_params
      }

      logger.warn(data)
    end

    def sanitized_pipeline_params
      pipeline_params.except(:push_options)
    end

    def logger
      if Gitlab::Runtime.sidekiq?
        Sidekiq.logger
      else
        # This service runs in Sidekiq, so this shouldn't ever be
        # called, but this is included just in case.
        Gitlab::ProjectServiceLogger
      end
    end
  end
end