datadog-ci 1.17.0 → 1.19.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/CHANGELOG.md +28 -2
- data/ext/datadog_ci_native/ci.c +10 -0
- data/ext/{datadog_cov → datadog_ci_native}/datadog_cov.c +119 -147
- data/ext/datadog_ci_native/datadog_cov.h +3 -0
- data/ext/datadog_ci_native/datadog_source_code.c +28 -0
- data/ext/datadog_ci_native/datadog_source_code.h +3 -0
- data/ext/{datadog_cov → datadog_ci_native}/extconf.rb +1 -1
- data/lib/datadog/ci/contrib/minitest/test.rb +17 -7
- data/lib/datadog/ci/contrib/rspec/example.rb +14 -7
- data/lib/datadog/ci/ext/environment/providers/appveyor.rb +1 -1
- data/lib/datadog/ci/ext/environment/providers/buildkite.rb +4 -0
- data/lib/datadog/ci/ext/environment/providers/gitlab.rb +0 -4
- data/lib/datadog/ci/ext/telemetry.rb +1 -2
- data/lib/datadog/ci/ext/test.rb +1 -0
- data/lib/datadog/ci/git/base_branch_sha_detection/base.rb +66 -0
- data/lib/datadog/ci/git/base_branch_sha_detection/branch_metric.rb +34 -0
- data/lib/datadog/ci/git/base_branch_sha_detection/guesser.rb +137 -0
- data/lib/datadog/ci/git/base_branch_sha_detection/merge_base_extractor.rb +29 -0
- data/lib/datadog/ci/git/base_branch_sha_detector.rb +63 -0
- data/lib/datadog/ci/git/changed_lines.rb +109 -0
- data/lib/datadog/ci/git/cli.rb +56 -0
- data/lib/datadog/ci/git/diff.rb +90 -0
- data/lib/datadog/ci/git/local_repository.rb +94 -321
- data/lib/datadog/ci/git/telemetry.rb +14 -0
- data/lib/datadog/ci/impacted_tests_detection/component.rb +15 -11
- data/lib/datadog/ci/test.rb +16 -0
- data/lib/datadog/ci/test_optimisation/component.rb +10 -6
- data/lib/datadog/ci/test_optimisation/coverage/ddcov.rb +1 -1
- data/lib/datadog/ci/test_visibility/telemetry.rb +3 -0
- data/lib/datadog/ci/utils/command.rb +117 -0
- data/lib/datadog/ci/utils/source_code.rb +31 -0
- data/lib/datadog/ci/version.rb +1 -1
- metadata +18 -5
- data/lib/datadog/ci/impacted_tests_detection/telemetry.rb +0 -16
@@ -5,6 +5,10 @@ require "pathname"
|
|
5
5
|
require "set"
|
6
6
|
|
7
7
|
require_relative "../ext/telemetry"
|
8
|
+
require_relative "../utils/command"
|
9
|
+
require_relative "base_branch_sha_detector"
|
10
|
+
require_relative "cli"
|
11
|
+
require_relative "diff"
|
8
12
|
require_relative "telemetry"
|
9
13
|
require_relative "user"
|
10
14
|
|
@@ -12,21 +16,6 @@ module Datadog
|
|
12
16
|
module CI
|
13
17
|
module Git
|
14
18
|
module LocalRepository
|
15
|
-
class GitCommandExecutionError < StandardError
|
16
|
-
attr_reader :output, :command, :status
|
17
|
-
def initialize(message, output:, command:, status:)
|
18
|
-
super(message)
|
19
|
-
|
20
|
-
@output = output
|
21
|
-
@command = command
|
22
|
-
@status = status
|
23
|
-
end
|
24
|
-
end
|
25
|
-
|
26
|
-
COMMAND_RETRY_COUNT = 3
|
27
|
-
POSSIBLE_BASE_BRANCHES = %w[main master preprod prod dev development trunk].freeze
|
28
|
-
DEFAULT_LIKE_BRANCH_FILTER = /^(#{POSSIBLE_BASE_BRANCHES.join("|")}|release\/.*|hotfix\/.*)$/.freeze
|
29
|
-
|
30
19
|
def self.root
|
31
20
|
return @root if defined?(@root)
|
32
21
|
|
@@ -34,7 +23,7 @@ module Datadog
|
|
34
23
|
end
|
35
24
|
|
36
25
|
# ATTENTION: this function is running in a hot path
|
37
|
-
# and
|
26
|
+
# and must be optimized for performance
|
38
27
|
def self.relative_to_root(path)
|
39
28
|
return "" if path.nil?
|
40
29
|
|
@@ -49,14 +38,22 @@ module Datadog
|
|
49
38
|
# that root is a prefix of the path
|
50
39
|
return "" if path.size < prefix_index
|
51
40
|
|
52
|
-
|
53
|
-
|
41
|
+
# this means that the root is not a prefix of this path somehow
|
42
|
+
return "" if path[prefix_index] != File::SEPARATOR
|
43
|
+
|
44
|
+
res = path[prefix_index + 1..]
|
54
45
|
else
|
55
46
|
# prefix_to_root is a difference between the root path and the given path
|
56
|
-
if @prefix_to_root
|
57
|
-
|
58
|
-
|
59
|
-
|
47
|
+
if defined?(@prefix_to_root)
|
48
|
+
# if path starts with ./ remove the dot before applying the optimization
|
49
|
+
# @type var path: String
|
50
|
+
path = path[1..] if path.start_with?("./")
|
51
|
+
|
52
|
+
if @prefix_to_root == ""
|
53
|
+
return path
|
54
|
+
elsif @prefix_to_root
|
55
|
+
return File.join(@prefix_to_root, path)
|
56
|
+
end
|
60
57
|
end
|
61
58
|
|
62
59
|
pathname = Pathname.new(File.expand_path(path))
|
@@ -97,26 +94,26 @@ module Datadog
|
|
97
94
|
res = nil
|
98
95
|
|
99
96
|
duration_ms = Core::Utils::Time.measure(:float_millisecond) do
|
100
|
-
res = exec_git_command("
|
97
|
+
res = CLI.exec_git_command(["ls-remote", "--get-url"])
|
101
98
|
end
|
102
99
|
|
103
100
|
Telemetry.git_command_ms(Ext::Telemetry::Command::GET_REPOSITORY, duration_ms)
|
104
101
|
res
|
105
102
|
rescue => e
|
106
103
|
log_failure(e, "git repository url")
|
107
|
-
|
104
|
+
Telemetry.track_error(e, Ext::Telemetry::Command::GET_REPOSITORY)
|
108
105
|
nil
|
109
106
|
end
|
110
107
|
|
111
108
|
def self.git_root
|
112
|
-
exec_git_command("
|
109
|
+
CLI.exec_git_command(["rev-parse", "--show-toplevel"])
|
113
110
|
rescue => e
|
114
111
|
log_failure(e, "git root path")
|
115
112
|
nil
|
116
113
|
end
|
117
114
|
|
118
115
|
def self.git_commit_sha
|
119
|
-
exec_git_command("
|
116
|
+
CLI.exec_git_command(["rev-parse", "HEAD"])
|
120
117
|
rescue => e
|
121
118
|
log_failure(e, "git commit sha")
|
122
119
|
nil
|
@@ -128,26 +125,26 @@ module Datadog
|
|
128
125
|
res = nil
|
129
126
|
|
130
127
|
duration_ms = Core::Utils::Time.measure(:float_millisecond) do
|
131
|
-
res = exec_git_command("
|
128
|
+
res = CLI.exec_git_command(["rev-parse", "--abbrev-ref", "HEAD"])
|
132
129
|
end
|
133
130
|
|
134
131
|
Telemetry.git_command_ms(Ext::Telemetry::Command::GET_BRANCH, duration_ms)
|
135
132
|
res
|
136
133
|
rescue => e
|
137
134
|
log_failure(e, "git branch")
|
138
|
-
|
135
|
+
Telemetry.track_error(e, Ext::Telemetry::Command::GET_BRANCH)
|
139
136
|
nil
|
140
137
|
end
|
141
138
|
|
142
139
|
def self.git_tag
|
143
|
-
exec_git_command("
|
140
|
+
CLI.exec_git_command(["tag", "--points-at", "HEAD"])
|
144
141
|
rescue => e
|
145
142
|
log_failure(e, "git tag")
|
146
143
|
nil
|
147
144
|
end
|
148
145
|
|
149
146
|
def self.git_commit_message
|
150
|
-
exec_git_command("
|
147
|
+
CLI.exec_git_command(["log", "-n", "1", "--format=%B"])
|
151
148
|
rescue => e
|
152
149
|
log_failure(e, "git commit message")
|
153
150
|
nil
|
@@ -155,7 +152,7 @@ module Datadog
|
|
155
152
|
|
156
153
|
def self.git_commit_users
|
157
154
|
# Get committer and author information in one command.
|
158
|
-
output = exec_git_command("
|
155
|
+
output = CLI.exec_git_command(["show", "-s", "--format=%an\t%ae\t%at\t%cn\t%ce\t%ct"])
|
159
156
|
unless output
|
160
157
|
Datadog.logger.debug(
|
161
158
|
"Unable to read git commit users: git command output is nil"
|
@@ -186,7 +183,7 @@ module Datadog
|
|
186
183
|
output = nil
|
187
184
|
|
188
185
|
duration_ms = Core::Utils::Time.measure(:float_millisecond) do
|
189
|
-
output = exec_git_command("
|
186
|
+
output = CLI.exec_git_command(["log", "--format=%H", "-n", "1000", "--since=\"1 month ago\""], timeout: CLI::LONG_TIMEOUT)
|
190
187
|
end
|
191
188
|
|
192
189
|
Telemetry.git_command_ms(Ext::Telemetry::Command::GET_LOCAL_COMMITS, duration_ms)
|
@@ -196,27 +193,28 @@ module Datadog
|
|
196
193
|
output.split("\n")
|
197
194
|
rescue => e
|
198
195
|
log_failure(e, "git commits")
|
199
|
-
|
196
|
+
Telemetry.track_error(e, Ext::Telemetry::Command::GET_LOCAL_COMMITS)
|
200
197
|
[]
|
201
198
|
end
|
202
199
|
|
203
200
|
def self.git_commits_rev_list(included_commits:, excluded_commits:)
|
204
201
|
Telemetry.git_command(Ext::Telemetry::Command::GET_OBJECTS)
|
205
|
-
|
206
|
-
|
202
|
+
included_commits_list = filter_invalid_commits(included_commits)
|
203
|
+
excluded_commits_list = filter_invalid_commits(excluded_commits).map { |sha| "^#{sha}" }
|
207
204
|
|
208
205
|
# @type var res: String?
|
209
206
|
res = nil
|
210
207
|
|
211
208
|
duration_ms = Core::Utils::Time.measure(:float_millisecond) do
|
212
|
-
|
213
|
-
"
|
214
|
-
"--objects
|
215
|
-
"--no-object-names
|
216
|
-
"--filter=blob:none
|
217
|
-
"--since=\"1 month ago\"
|
218
|
-
|
219
|
-
|
209
|
+
cmd = [
|
210
|
+
"rev-list",
|
211
|
+
"--objects",
|
212
|
+
"--no-object-names",
|
213
|
+
"--filter=blob:none",
|
214
|
+
"--since=\"1 month ago\""
|
215
|
+
] + excluded_commits_list + included_commits_list
|
216
|
+
|
217
|
+
res = CLI.exec_git_command(cmd, timeout: CLI::LONG_TIMEOUT)
|
220
218
|
end
|
221
219
|
|
222
220
|
Telemetry.git_command_ms(Ext::Telemetry::Command::GET_OBJECTS, duration_ms)
|
@@ -224,7 +222,7 @@ module Datadog
|
|
224
222
|
res
|
225
223
|
rescue => e
|
226
224
|
log_failure(e, "git commits rev list")
|
227
|
-
|
225
|
+
Telemetry.track_error(e, Ext::Telemetry::Command::GET_OBJECTS)
|
228
226
|
nil
|
229
227
|
end
|
230
228
|
|
@@ -239,9 +237,10 @@ module Datadog
|
|
239
237
|
Telemetry.git_command(Ext::Telemetry::Command::PACK_OBJECTS)
|
240
238
|
|
241
239
|
duration_ms = Core::Utils::Time.measure(:float_millisecond) do
|
242
|
-
exec_git_command(
|
243
|
-
"
|
244
|
-
stdin: commit_tree
|
240
|
+
CLI.exec_git_command(
|
241
|
+
["pack-objects", "--compression=9", "--max-pack-size=3m", "#{path}/#{basename}"],
|
242
|
+
stdin: commit_tree,
|
243
|
+
timeout: CLI::LONG_TIMEOUT
|
245
244
|
)
|
246
245
|
end
|
247
246
|
Telemetry.git_command_ms(Ext::Telemetry::Command::PACK_OBJECTS, duration_ms)
|
@@ -249,7 +248,7 @@ module Datadog
|
|
249
248
|
basename
|
250
249
|
rescue => e
|
251
250
|
log_failure(e, "git generate packfiles")
|
252
|
-
|
251
|
+
Telemetry.track_error(e, Ext::Telemetry::Command::PACK_OBJECTS)
|
253
252
|
nil
|
254
253
|
end
|
255
254
|
|
@@ -258,14 +257,14 @@ module Datadog
|
|
258
257
|
res = false
|
259
258
|
|
260
259
|
duration_ms = Core::Utils::Time.measure(:float_millisecond) do
|
261
|
-
res = exec_git_command("
|
260
|
+
res = CLI.exec_git_command(["rev-parse", "--is-shallow-repository"]) == "true"
|
262
261
|
end
|
263
262
|
Telemetry.git_command_ms(Ext::Telemetry::Command::CHECK_SHALLOW, duration_ms)
|
264
263
|
|
265
264
|
res
|
266
265
|
rescue => e
|
267
266
|
log_failure(e, "git shallow clone")
|
268
|
-
|
267
|
+
Telemetry.track_error(e, Ext::Telemetry::Command::CHECK_SHALLOW)
|
269
268
|
false
|
270
269
|
end
|
271
270
|
|
@@ -274,19 +273,15 @@ module Datadog
|
|
274
273
|
# @type var res: String?
|
275
274
|
res = nil
|
276
275
|
|
277
|
-
|
278
|
-
|
279
|
-
|
280
|
-
"--update-shallow " \
|
281
|
-
"--filter=\"blob:none\" " \
|
282
|
-
"--recurse-submodules=no " \
|
283
|
-
"$(git config --default origin --get clone.defaultRemoteName)"
|
276
|
+
default_remote = CLI.exec_git_command(["config", "--default", "origin", "--get", "clone.defaultRemoteName"])&.strip
|
277
|
+
head_commit = git_commit_sha
|
278
|
+
upstream_branch = get_upstream_branch
|
284
279
|
|
285
|
-
|
286
|
-
|
287
|
-
|
288
|
-
|
289
|
-
|
280
|
+
# Build array of remotes to try, filtering out nil values
|
281
|
+
unshallow_remotes = []
|
282
|
+
unshallow_remotes << head_commit if head_commit
|
283
|
+
unshallow_remotes << upstream_branch if upstream_branch
|
284
|
+
unshallow_remotes << nil # Ensure the loop runs at least once, even if no valid remotes are available. This acts as a fallback mechanism.
|
290
285
|
|
291
286
|
duration_ms = Core::Utils::Time.measure(:float_millisecond) do
|
292
287
|
unshallow_remotes.each do |remote|
|
@@ -294,17 +289,27 @@ module Datadog
|
|
294
289
|
|
295
290
|
res =
|
296
291
|
begin
|
297
|
-
|
298
|
-
|
299
|
-
|
292
|
+
# @type var cmd: Array[String]
|
293
|
+
cmd = [
|
294
|
+
"fetch",
|
295
|
+
"--shallow-since=\"1 month ago\"",
|
296
|
+
"--update-shallow",
|
297
|
+
"--filter=blob:none",
|
298
|
+
"--recurse-submodules=no",
|
299
|
+
default_remote
|
300
|
+
]
|
301
|
+
cmd << remote if remote
|
302
|
+
|
303
|
+
CLI.exec_git_command(cmd, timeout: CLI::UNSHALLOW_TIMEOUT)
|
300
304
|
rescue => e
|
301
305
|
log_failure(e, "git unshallow")
|
302
|
-
|
306
|
+
Telemetry.track_error(e, Ext::Telemetry::Command::UNSHALLOW)
|
303
307
|
unshallowing_errored = true
|
304
308
|
nil
|
305
309
|
end
|
306
310
|
|
307
|
-
break
|
311
|
+
# If the command succeeded, break and return the result
|
312
|
+
break [] if res && !unshallowing_errored
|
308
313
|
end
|
309
314
|
end
|
310
315
|
|
@@ -312,10 +317,10 @@ module Datadog
|
|
312
317
|
res
|
313
318
|
end
|
314
319
|
|
315
|
-
# Returns a
|
320
|
+
# Returns a Diff object with relative file paths for files that were changed since the given base_commit.
|
316
321
|
# If base_commit is nil, returns nil. On error, returns nil.
|
317
|
-
def self.
|
318
|
-
return
|
322
|
+
def self.get_changes_since(base_commit)
|
323
|
+
return Diff.new if base_commit.nil?
|
319
324
|
|
320
325
|
Datadog.logger.debug { "calculating git diff from base_commit: #{base_commit}" }
|
321
326
|
|
@@ -327,35 +332,20 @@ module Datadog
|
|
327
332
|
# @type var output: String?
|
328
333
|
output = nil
|
329
334
|
duration_ms = Core::Utils::Time.measure(:float_millisecond) do
|
330
|
-
output = exec_git_command("
|
335
|
+
output = CLI.exec_git_command(["diff", "-U0", "--word-diff=porcelain", base_commit, "HEAD"], timeout: CLI::LONG_TIMEOUT)
|
331
336
|
end
|
332
337
|
Telemetry.git_command_ms(Ext::Telemetry::Command::DIFF, duration_ms)
|
333
338
|
|
334
339
|
Datadog.logger.debug { "git diff output: #{output}" }
|
335
340
|
|
336
|
-
return
|
337
|
-
|
338
|
-
# 2. Parse the output
|
339
|
-
|
340
|
-
output.each_line do |line|
|
341
|
-
# Match lines like: diff --git a/foo/bar.rb b/foo/bar.rb
|
342
|
-
# This captures git changes on file level
|
343
|
-
match = /^diff --git a\/(?<file>.+) b\/(?<file2>.+)$/.match(line)
|
344
|
-
if match && match[:file]
|
345
|
-
changed_file = match[:file]
|
346
|
-
# Normalize to repo root
|
347
|
-
normalized_changed_file = relative_to_root(changed_file)
|
348
|
-
changed_files << normalized_changed_file unless normalized_changed_file.nil? || normalized_changed_file.empty?
|
349
|
-
|
350
|
-
Datadog.logger.debug { "matched changed_file: #{changed_file} from line: #{line}" }
|
351
|
-
Datadog.logger.debug { "normalized_changed_file: #{normalized_changed_file}" }
|
352
|
-
end
|
353
|
-
end
|
354
|
-
changed_files
|
341
|
+
return Diff.new if output.nil?
|
342
|
+
|
343
|
+
# 2. Parse the output using Git::Diff
|
344
|
+
Diff.parse_diff_output(output)
|
355
345
|
rescue => e
|
356
|
-
|
346
|
+
Telemetry.track_error(e, Ext::Telemetry::Command::DIFF)
|
357
347
|
log_failure(e, "get changed files from diff")
|
358
|
-
|
348
|
+
Diff.new
|
359
349
|
end
|
360
350
|
end
|
361
351
|
|
@@ -364,245 +354,28 @@ module Datadog
|
|
364
354
|
def self.base_commit_sha(base_branch: nil)
|
365
355
|
Telemetry.git_command(Ext::Telemetry::Command::BASE_COMMIT_SHA)
|
366
356
|
|
367
|
-
|
368
|
-
Datadog.logger.debug { "Remote name: '#{remote_name}'" }
|
369
|
-
|
370
|
-
source_branch = get_source_branch
|
371
|
-
return nil if source_branch.nil?
|
372
|
-
|
373
|
-
Datadog.logger.debug { "Source branch: '#{source_branch}'" }
|
374
|
-
|
375
|
-
# Early exit if source is a main-like branch
|
376
|
-
if main_like_branch?(source_branch, remote_name)
|
377
|
-
Datadog.logger.debug { "Branch '#{source_branch}' already matches base branch filter (#{DEFAULT_LIKE_BRANCH_FILTER})" }
|
378
|
-
return nil
|
379
|
-
end
|
380
|
-
|
381
|
-
possible_base_branches = base_branch.nil? ? POSSIBLE_BASE_BRANCHES : [base_branch]
|
382
|
-
# Check and fetch base branches if they don't exist in local git repository
|
383
|
-
check_and_fetch_base_branches(possible_base_branches, remote_name)
|
384
|
-
|
385
|
-
default_branch = detect_default_branch(remote_name)
|
386
|
-
Datadog.logger.debug { "Default branch: '#{default_branch}'" }
|
387
|
-
|
388
|
-
candidates = build_candidate_list(remote_name, source_branch, base_branch)
|
389
|
-
if candidates.nil? || candidates.empty?
|
390
|
-
Datadog.logger.debug { "No candidate branches found." }
|
391
|
-
return nil
|
392
|
-
end
|
393
|
-
|
394
|
-
metrics = compute_branch_metrics(candidates, source_branch)
|
395
|
-
Datadog.logger.debug { "Branch metrics: '#{metrics}'" }
|
396
|
-
|
397
|
-
best_branch_sha = find_best_branch(metrics, default_branch, remote_name)
|
398
|
-
Datadog.logger.debug { "Best branch: '#{best_branch_sha}'" }
|
399
|
-
|
400
|
-
best_branch_sha
|
357
|
+
BaseBranchShaDetector.base_branch_sha(base_branch)
|
401
358
|
rescue => e
|
402
|
-
|
359
|
+
Telemetry.track_error(e, Ext::Telemetry::Command::BASE_COMMIT_SHA)
|
403
360
|
log_failure(e, "git base ref")
|
404
361
|
nil
|
405
362
|
end
|
406
363
|
|
407
|
-
def self.
|
408
|
-
|
409
|
-
|
410
|
-
Datadog.logger.debug { "
|
411
|
-
rescue GitCommandExecutionError => e
|
412
|
-
Datadog.logger.debug { "Branch '#{branch}' doesn't exist locally, checking remote: #{e}" }
|
413
|
-
begin
|
414
|
-
remote_heads = exec_git_command("git ls-remote --heads #{remote_name} #{branch}")
|
415
|
-
if remote_heads.nil? || remote_heads.empty?
|
416
|
-
Datadog.logger.debug { "Branch '#{branch}' doesn't exist in remote" }
|
417
|
-
return
|
418
|
-
end
|
419
|
-
|
420
|
-
Datadog.logger.debug { "Branch '#{branch}' exists in remote, fetching" }
|
421
|
-
exec_git_command("git fetch --depth 1 #{remote_name} #{branch}:#{branch}")
|
422
|
-
rescue GitCommandExecutionError => e
|
423
|
-
Datadog.logger.debug { "Branch '#{branch}' couldn't be fetched from remote: #{e}" }
|
424
|
-
end
|
425
|
-
end
|
426
|
-
|
427
|
-
def self.check_and_fetch_base_branches(branches, remote_name)
|
428
|
-
branches.each do |branch|
|
429
|
-
check_and_fetch_branch(branch, remote_name)
|
430
|
-
end
|
431
|
-
end
|
432
|
-
|
433
|
-
def self.get_source_branch
|
434
|
-
source_branch = exec_git_command("git rev-parse --abbrev-ref HEAD")&.strip
|
435
|
-
if source_branch.nil?
|
436
|
-
Datadog.logger.debug { "Could not get current branch" }
|
437
|
-
return nil
|
438
|
-
end
|
439
|
-
|
440
|
-
exec_git_command("git rev-parse --verify --quiet #{source_branch} > /dev/null")
|
441
|
-
source_branch
|
442
|
-
end
|
443
|
-
|
444
|
-
def self.remove_remote_prefix(branch_name, remote_name)
|
445
|
-
branch_name&.sub(/^#{Regexp.escape(remote_name)}\//, "")
|
446
|
-
end
|
447
|
-
|
448
|
-
def self.main_like_branch?(branch_name, remote_name)
|
449
|
-
short_branch_name = remove_remote_prefix(branch_name, remote_name)
|
450
|
-
short_branch_name&.match?(DEFAULT_LIKE_BRANCH_FILTER)
|
451
|
-
end
|
452
|
-
|
453
|
-
def self.detect_default_branch(remote_name)
|
454
|
-
# @type var default_branch: String?
|
455
|
-
default_branch = nil
|
456
|
-
begin
|
457
|
-
default_ref = exec_git_command("git symbolic-ref --quiet --short \"refs/remotes/#{remote_name}/HEAD\" 2>/dev/null")
|
458
|
-
default_branch = remove_remote_prefix(default_ref, remote_name) unless default_ref.nil?
|
459
|
-
rescue
|
460
|
-
Datadog.logger.debug { "Could not get symbolic-ref, trying to find a fallback (main, master)..." }
|
461
|
-
end
|
462
|
-
|
463
|
-
default_branch = find_fallback_default_branch(remote_name) if default_branch.nil?
|
464
|
-
default_branch
|
465
|
-
end
|
466
|
-
|
467
|
-
def self.find_fallback_default_branch(remote_name)
|
468
|
-
["main", "master"].each do |fallback|
|
469
|
-
exec_git_command("git show-ref --verify --quiet \"refs/remotes/#{remote_name}/#{fallback}\"")
|
470
|
-
Datadog.logger.debug { "Found fallback default branch '#{fallback}'" }
|
471
|
-
return fallback
|
472
|
-
rescue
|
473
|
-
next
|
474
|
-
end
|
364
|
+
def self.get_upstream_branch
|
365
|
+
CLI.exec_git_command(["rev-parse", "--abbrev-ref", "--symbolic-full-name", "@{upstream}"])
|
366
|
+
rescue => e
|
367
|
+
Datadog.logger.debug { "Error getting upstream: #{e}" }
|
475
368
|
nil
|
476
369
|
end
|
477
370
|
|
478
|
-
def self.
|
479
|
-
|
480
|
-
return [base_branch]
|
481
|
-
end
|
482
|
-
|
483
|
-
candidates = exec_git_command("git for-each-ref --format='%(refname:short)' refs/heads \"refs/remotes/#{remote_name}\"")&.lines&.map(&:strip)
|
484
|
-
Datadog.logger.debug { "Available branches: '#{candidates}'" }
|
485
|
-
candidates&.select! { |b| b.match?(DEFAULT_LIKE_BRANCH_FILTER) && b != source_branch }
|
486
|
-
Datadog.logger.debug { "Candidate branches: '#{candidates}'" }
|
487
|
-
candidates
|
488
|
-
end
|
489
|
-
|
490
|
-
def self.compute_branch_metrics(candidates, source_branch)
|
491
|
-
metrics = {}
|
492
|
-
candidates.each do |cand|
|
493
|
-
base_sha = exec_git_command("git merge-base #{cand} #{source_branch} 2>/dev/null")&.strip
|
494
|
-
next if base_sha.nil? || base_sha.empty?
|
495
|
-
|
496
|
-
behind, ahead = exec_git_command("git rev-list --left-right --count #{cand}...#{source_branch}")&.strip&.split&.map(&:to_i)
|
497
|
-
metrics[cand] = {behind: behind, ahead: ahead, base_sha: base_sha}
|
498
|
-
end
|
499
|
-
metrics
|
500
|
-
end
|
501
|
-
|
502
|
-
def self.find_best_branch(metrics, default_branch, remote_name)
|
503
|
-
return nil if metrics.empty?
|
504
|
-
|
505
|
-
_, best_data = metrics.min_by do |cand, data|
|
506
|
-
[
|
507
|
-
data[:ahead],
|
508
|
-
default_branch?(cand, default_branch, remote_name) ? 0 : 1 # prefer default branch on tie
|
509
|
-
]
|
510
|
-
end
|
511
|
-
|
512
|
-
best_data ? best_data[:base_sha] : nil
|
513
|
-
end
|
514
|
-
|
515
|
-
def self.default_branch?(branch, default_branch, remote_name)
|
516
|
-
branch == default_branch || branch == "#{remote_name}/#{default_branch}"
|
517
|
-
end
|
518
|
-
|
519
|
-
def self.get_remote_name
|
520
|
-
# Try to find remote from upstream tracking
|
521
|
-
upstream = nil
|
522
|
-
begin
|
523
|
-
upstream = exec_git_command("git rev-parse --abbrev-ref --symbolic-full-name @{upstream}")&.strip
|
524
|
-
rescue => e
|
525
|
-
Datadog.logger.debug { "Error getting upstream: #{e}" }
|
526
|
-
end
|
527
|
-
|
528
|
-
if upstream
|
529
|
-
upstream.split("/").first
|
530
|
-
else
|
531
|
-
# Fallback to first remote if no upstream is set
|
532
|
-
first_remote_value = exec_git_command("git remote")&.split("\n")&.first
|
533
|
-
Datadog.logger.debug { "First remote value: '#{first_remote_value}'" }
|
534
|
-
first_remote_value || "origin"
|
535
|
-
end
|
371
|
+
def self.filter_invalid_commits(commits)
|
372
|
+
commits.filter { |commit| Utils::Git.valid_commit_sha?(commit) }
|
536
373
|
end
|
537
374
|
|
538
|
-
|
539
|
-
|
540
|
-
|
541
|
-
|
542
|
-
|
543
|
-
def filter_invalid_commits(commits)
|
544
|
-
commits.filter { |commit| Utils::Git.valid_commit_sha?(commit) }
|
545
|
-
end
|
546
|
-
|
547
|
-
def exec_git_command(cmd, stdin: nil)
|
548
|
-
# Shell injection is alleviated by making sure that no outside modules call this method.
|
549
|
-
# It is called only internally with static parameters.
|
550
|
-
|
551
|
-
# @type var out: String
|
552
|
-
# @type var status: Process::Status?
|
553
|
-
out, status = Open3.capture2e(cmd, stdin_data: stdin)
|
554
|
-
|
555
|
-
if status.nil?
|
556
|
-
# @type var retry_count: Integer
|
557
|
-
retry_count = COMMAND_RETRY_COUNT
|
558
|
-
Datadog.logger.debug { "Opening pipe failed, starting retries..." }
|
559
|
-
while status.nil? && retry_count.positive?
|
560
|
-
# no-dd-sa:ruby-security/shell-injection
|
561
|
-
out, status = Open3.capture2e(cmd, stdin_data: stdin)
|
562
|
-
Datadog.logger.debug { "After retry status is [#{status}]" }
|
563
|
-
retry_count -= 1
|
564
|
-
end
|
565
|
-
end
|
566
|
-
|
567
|
-
if status.nil? || !status.success?
|
568
|
-
raise GitCommandExecutionError.new(
|
569
|
-
"Failed to run git command [#{cmd}] with input [#{stdin}] and output [#{out}]",
|
570
|
-
output: out,
|
571
|
-
command: cmd,
|
572
|
-
status: status
|
573
|
-
)
|
574
|
-
end
|
575
|
-
|
576
|
-
# Sometimes Encoding.default_external is somehow set to US-ASCII which breaks
|
577
|
-
# commit messages with UTF-8 characters like emojis
|
578
|
-
# We force output's encoding to be UTF-8 in this case
|
579
|
-
# This is safe to do as UTF-8 is compatible with US-ASCII
|
580
|
-
if Encoding.default_external == Encoding::US_ASCII
|
581
|
-
out = out.force_encoding(Encoding::UTF_8)
|
582
|
-
end
|
583
|
-
out.strip! # There's always a "\n" at the end of the command output
|
584
|
-
|
585
|
-
return nil if out.empty?
|
586
|
-
|
587
|
-
out
|
588
|
-
end
|
589
|
-
|
590
|
-
def log_failure(e, action)
|
591
|
-
Datadog.logger.debug(
|
592
|
-
"Unable to perform #{action}: #{e.class.name} #{e.message} at #{Array(e.backtrace).first}"
|
593
|
-
)
|
594
|
-
end
|
595
|
-
|
596
|
-
def telemetry_track_error(e, command)
|
597
|
-
case e
|
598
|
-
when Errno::ENOENT
|
599
|
-
Telemetry.git_command_errors(command, executable_missing: true)
|
600
|
-
when GitCommandExecutionError
|
601
|
-
Telemetry.git_command_errors(command, exit_code: e.status&.to_i)
|
602
|
-
else
|
603
|
-
Telemetry.git_command_errors(command, exit_code: -9000)
|
604
|
-
end
|
605
|
-
end
|
375
|
+
def self.log_failure(e, action)
|
376
|
+
Datadog.logger.debug(
|
377
|
+
"Unable to perform #{action}: #{e.class.name} #{e.message} at #{Array(e.backtrace).first}"
|
378
|
+
)
|
606
379
|
end
|
607
380
|
end
|
608
381
|
end
|
@@ -1,5 +1,8 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
+
require_relative "../utils/telemetry"
|
4
|
+
require_relative "cli"
|
5
|
+
|
3
6
|
module Datadog
|
4
7
|
module CI
|
5
8
|
module Git
|
@@ -21,6 +24,17 @@ module Datadog
|
|
21
24
|
Utils::Telemetry.distribution(Ext::Telemetry::METRIC_GIT_COMMAND_MS, duration_ms, tags_for_command(command))
|
22
25
|
end
|
23
26
|
|
27
|
+
def self.track_error(e, command)
|
28
|
+
case e
|
29
|
+
when Errno::ENOENT
|
30
|
+
git_command_errors(command, executable_missing: true)
|
31
|
+
when CLI::GitCommandExecutionError
|
32
|
+
git_command_errors(command, exit_code: e.status&.to_i)
|
33
|
+
else
|
34
|
+
git_command_errors(command, exit_code: -9000)
|
35
|
+
end
|
36
|
+
end
|
37
|
+
|
24
38
|
def self.tags_for_command(command)
|
25
39
|
{Ext::Telemetry::TAG_COMMAND => command}
|
26
40
|
end
|