datadog-ci 1.16.0 → 1.18.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.
Files changed (59) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +37 -2
  3. data/ext/datadog_ci_native/ci.c +10 -0
  4. data/ext/{datadog_cov → datadog_ci_native}/datadog_cov.c +119 -147
  5. data/ext/datadog_ci_native/datadog_cov.h +3 -0
  6. data/ext/datadog_ci_native/datadog_source_code.c +28 -0
  7. data/ext/datadog_ci_native/datadog_source_code.h +3 -0
  8. data/ext/{datadog_cov → datadog_ci_native}/extconf.rb +1 -1
  9. data/lib/datadog/ci/codeowners/rule.rb +5 -0
  10. data/lib/datadog/ci/configuration/components.rb +17 -5
  11. data/lib/datadog/ci/configuration/settings.rb +6 -0
  12. data/lib/datadog/ci/contrib/knapsack/patcher.rb +1 -3
  13. data/lib/datadog/ci/contrib/knapsack/runner.rb +2 -0
  14. data/lib/datadog/ci/contrib/minitest/runner.rb +1 -0
  15. data/lib/datadog/ci/contrib/minitest/test.rb +24 -9
  16. data/lib/datadog/ci/contrib/parallel_tests/patcher.rb +1 -3
  17. data/lib/datadog/ci/contrib/patcher.rb +4 -0
  18. data/lib/datadog/ci/contrib/rspec/example.rb +14 -7
  19. data/lib/datadog/ci/contrib/rspec/helpers.rb +1 -3
  20. data/lib/datadog/ci/ext/environment/extractor.rb +4 -6
  21. data/lib/datadog/ci/ext/environment/providers/appveyor.rb +5 -0
  22. data/lib/datadog/ci/ext/environment/providers/base.rb +7 -2
  23. data/lib/datadog/ci/ext/environment/providers/bitbucket.rb +6 -0
  24. data/lib/datadog/ci/ext/environment/providers/bitrise.rb +7 -1
  25. data/lib/datadog/ci/ext/environment/providers/buddy.rb +5 -0
  26. data/lib/datadog/ci/ext/environment/providers/github_actions.rb +37 -18
  27. data/lib/datadog/ci/ext/environment/providers/gitlab.rb +13 -1
  28. data/lib/datadog/ci/ext/environment/providers/user_defined_tags.rb +12 -0
  29. data/lib/datadog/ci/ext/git.rb +3 -0
  30. data/lib/datadog/ci/ext/settings.rb +1 -0
  31. data/lib/datadog/ci/ext/telemetry.rb +3 -0
  32. data/lib/datadog/ci/ext/test.rb +5 -1
  33. data/lib/datadog/ci/ext/transport.rb +1 -0
  34. data/lib/datadog/ci/git/base_branch_sha_detection/base.rb +66 -0
  35. data/lib/datadog/ci/git/base_branch_sha_detection/branch_metric.rb +34 -0
  36. data/lib/datadog/ci/git/base_branch_sha_detection/guesser.rb +137 -0
  37. data/lib/datadog/ci/git/base_branch_sha_detection/merge_base_extractor.rb +29 -0
  38. data/lib/datadog/ci/git/base_branch_sha_detector.rb +63 -0
  39. data/lib/datadog/ci/git/cli.rb +56 -0
  40. data/lib/datadog/ci/git/local_repository.rb +131 -118
  41. data/lib/datadog/ci/git/telemetry.rb +14 -0
  42. data/lib/datadog/ci/git/tree_uploader.rb +10 -3
  43. data/lib/datadog/ci/impacted_tests_detection/component.rb +81 -0
  44. data/lib/datadog/ci/remote/component.rb +6 -1
  45. data/lib/datadog/ci/remote/library_settings.rb +8 -0
  46. data/lib/datadog/ci/span.rb +7 -0
  47. data/lib/datadog/ci/test.rb +6 -0
  48. data/lib/datadog/ci/test_management/tests_properties.rb +2 -1
  49. data/lib/datadog/ci/test_optimisation/component.rb +10 -6
  50. data/lib/datadog/ci/test_optimisation/coverage/ddcov.rb +1 -1
  51. data/lib/datadog/ci/test_retries/component.rb +8 -17
  52. data/lib/datadog/ci/test_retries/driver/{retry_new.rb → retry_flake_detection.rb} +1 -1
  53. data/lib/datadog/ci/test_retries/strategy/{retry_new.rb → retry_flake_detection.rb} +4 -4
  54. data/lib/datadog/ci/test_visibility/component.rb +6 -0
  55. data/lib/datadog/ci/test_visibility/telemetry.rb +3 -0
  56. data/lib/datadog/ci/utils/command.rb +116 -0
  57. data/lib/datadog/ci/utils/source_code.rb +31 -0
  58. data/lib/datadog/ci/version.rb +1 -1
  59. metadata +21 -8
@@ -2,8 +2,12 @@
2
2
 
3
3
  require "open3"
4
4
  require "pathname"
5
+ require "set"
5
6
 
6
7
  require_relative "../ext/telemetry"
8
+ require_relative "../utils/command"
9
+ require_relative "base_branch_sha_detector"
10
+ require_relative "cli"
7
11
  require_relative "telemetry"
8
12
  require_relative "user"
9
13
 
@@ -11,19 +15,6 @@ module Datadog
11
15
  module CI
12
16
  module Git
13
17
  module LocalRepository
14
- class GitCommandExecutionError < StandardError
15
- attr_reader :output, :command, :status
16
- def initialize(message, output:, command:, status:)
17
- super(message)
18
-
19
- @output = output
20
- @command = command
21
- @status = status
22
- end
23
- end
24
-
25
- COMMAND_RETRY_COUNT = 3
26
-
27
18
  def self.root
28
19
  return @root if defined?(@root)
29
20
 
@@ -31,7 +22,7 @@ module Datadog
31
22
  end
32
23
 
33
24
  # ATTENTION: this function is running in a hot path
34
- # and should be optimized for performance
25
+ # and must be optimized for performance
35
26
  def self.relative_to_root(path)
36
27
  return "" if path.nil?
37
28
 
@@ -63,7 +54,7 @@ module Datadog
63
54
  res = pathname.relative_path_from(root_path).to_s
64
55
 
65
56
  unless defined?(@prefix_to_root)
66
- @prefix_to_root = res&.gsub(path, "") if res.end_with?(path)
57
+ @prefix_to_root = res.gsub(path, "") if res.end_with?(path)
67
58
  end
68
59
  end
69
60
 
@@ -90,29 +81,30 @@ module Datadog
90
81
 
91
82
  def self.git_repository_url
92
83
  Telemetry.git_command(Ext::Telemetry::Command::GET_REPOSITORY)
84
+ # @type var res: String?
93
85
  res = nil
94
86
 
95
87
  duration_ms = Core::Utils::Time.measure(:float_millisecond) do
96
- res = exec_git_command("git ls-remote --get-url")
88
+ res = CLI.exec_git_command(["ls-remote", "--get-url"])
97
89
  end
98
90
 
99
91
  Telemetry.git_command_ms(Ext::Telemetry::Command::GET_REPOSITORY, duration_ms)
100
92
  res
101
93
  rescue => e
102
94
  log_failure(e, "git repository url")
103
- telemetry_track_error(e, Ext::Telemetry::Command::GET_REPOSITORY)
95
+ Telemetry.track_error(e, Ext::Telemetry::Command::GET_REPOSITORY)
104
96
  nil
105
97
  end
106
98
 
107
99
  def self.git_root
108
- exec_git_command("git rev-parse --show-toplevel")
100
+ CLI.exec_git_command(["rev-parse", "--show-toplevel"])
109
101
  rescue => e
110
102
  log_failure(e, "git root path")
111
103
  nil
112
104
  end
113
105
 
114
106
  def self.git_commit_sha
115
- exec_git_command("git rev-parse HEAD")
107
+ CLI.exec_git_command(["rev-parse", "HEAD"])
116
108
  rescue => e
117
109
  log_failure(e, "git commit sha")
118
110
  nil
@@ -120,29 +112,30 @@ module Datadog
120
112
 
121
113
  def self.git_branch
122
114
  Telemetry.git_command(Ext::Telemetry::Command::GET_BRANCH)
115
+ # @type var res: String?
123
116
  res = nil
124
117
 
125
118
  duration_ms = Core::Utils::Time.measure(:float_millisecond) do
126
- res = exec_git_command("git rev-parse --abbrev-ref HEAD")
119
+ res = CLI.exec_git_command(["rev-parse", "--abbrev-ref", "HEAD"])
127
120
  end
128
121
 
129
122
  Telemetry.git_command_ms(Ext::Telemetry::Command::GET_BRANCH, duration_ms)
130
123
  res
131
124
  rescue => e
132
125
  log_failure(e, "git branch")
133
- telemetry_track_error(e, Ext::Telemetry::Command::GET_BRANCH)
126
+ Telemetry.track_error(e, Ext::Telemetry::Command::GET_BRANCH)
134
127
  nil
135
128
  end
136
129
 
137
130
  def self.git_tag
138
- exec_git_command("git tag --points-at HEAD")
131
+ CLI.exec_git_command(["tag", "--points-at", "HEAD"])
139
132
  rescue => e
140
133
  log_failure(e, "git tag")
141
134
  nil
142
135
  end
143
136
 
144
137
  def self.git_commit_message
145
- exec_git_command("git log -n 1 --format=%B")
138
+ CLI.exec_git_command(["log", "-n", "1", "--format=%B"])
146
139
  rescue => e
147
140
  log_failure(e, "git commit message")
148
141
  nil
@@ -150,7 +143,7 @@ module Datadog
150
143
 
151
144
  def self.git_commit_users
152
145
  # Get committer and author information in one command.
153
- output = exec_git_command("git show -s --format='%an\t%ae\t%at\t%cn\t%ce\t%ct'")
146
+ output = CLI.exec_git_command(["show", "-s", "--format=%an\t%ae\t%at\t%cn\t%ce\t%ct"])
154
147
  unless output
155
148
  Datadog.logger.debug(
156
149
  "Unable to read git commit users: git command output is nil"
@@ -177,39 +170,42 @@ module Datadog
177
170
  def self.git_commits
178
171
  Telemetry.git_command(Ext::Telemetry::Command::GET_LOCAL_COMMITS)
179
172
 
173
+ # @type var output: String?
180
174
  output = nil
175
+
181
176
  duration_ms = Core::Utils::Time.measure(:float_millisecond) do
182
- output = exec_git_command("git log --format=%H -n 1000 --since=\"1 month ago\"")
177
+ output = CLI.exec_git_command(["log", "--format=%H", "-n", "1000", "--since=\"1 month ago\""], timeout: CLI::LONG_TIMEOUT)
183
178
  end
184
179
 
185
180
  Telemetry.git_command_ms(Ext::Telemetry::Command::GET_LOCAL_COMMITS, duration_ms)
186
181
 
187
182
  return [] if output.nil?
188
183
 
189
- # @type var output: String
190
184
  output.split("\n")
191
185
  rescue => e
192
186
  log_failure(e, "git commits")
193
- telemetry_track_error(e, Ext::Telemetry::Command::GET_LOCAL_COMMITS)
187
+ Telemetry.track_error(e, Ext::Telemetry::Command::GET_LOCAL_COMMITS)
194
188
  []
195
189
  end
196
190
 
197
191
  def self.git_commits_rev_list(included_commits:, excluded_commits:)
198
192
  Telemetry.git_command(Ext::Telemetry::Command::GET_OBJECTS)
199
- included_commits = filter_invalid_commits(included_commits).join(" ")
200
- excluded_commits = filter_invalid_commits(excluded_commits).map! { |sha| "^#{sha}" }.join(" ")
193
+ included_commits_list = filter_invalid_commits(included_commits)
194
+ excluded_commits_list = filter_invalid_commits(excluded_commits).map { |sha| "^#{sha}" }
201
195
 
196
+ # @type var res: String?
202
197
  res = nil
203
198
 
204
199
  duration_ms = Core::Utils::Time.measure(:float_millisecond) do
205
- res = exec_git_command(
206
- "git rev-list " \
207
- "--objects " \
208
- "--no-object-names " \
209
- "--filter=blob:none " \
210
- "--since=\"1 month ago\" " \
211
- "#{excluded_commits} #{included_commits}"
212
- )
200
+ cmd = [
201
+ "rev-list",
202
+ "--objects",
203
+ "--no-object-names",
204
+ "--filter=blob:none",
205
+ "--since=\"1 month ago\""
206
+ ] + excluded_commits_list + included_commits_list
207
+
208
+ res = CLI.exec_git_command(cmd, timeout: CLI::LONG_TIMEOUT)
213
209
  end
214
210
 
215
211
  Telemetry.git_command_ms(Ext::Telemetry::Command::GET_OBJECTS, duration_ms)
@@ -217,7 +213,7 @@ module Datadog
217
213
  res
218
214
  rescue => e
219
215
  log_failure(e, "git commits rev list")
220
- telemetry_track_error(e, Ext::Telemetry::Command::GET_OBJECTS)
216
+ Telemetry.track_error(e, Ext::Telemetry::Command::GET_OBJECTS)
221
217
  nil
222
218
  end
223
219
 
@@ -232,9 +228,10 @@ module Datadog
232
228
  Telemetry.git_command(Ext::Telemetry::Command::PACK_OBJECTS)
233
229
 
234
230
  duration_ms = Core::Utils::Time.measure(:float_millisecond) do
235
- exec_git_command(
236
- "git pack-objects --compression=9 --max-pack-size=3m #{path}/#{basename}",
237
- stdin: commit_tree
231
+ CLI.exec_git_command(
232
+ ["pack-objects", "--compression=9", "--max-pack-size=3m", "#{path}/#{basename}"],
233
+ stdin: commit_tree,
234
+ timeout: CLI::LONG_TIMEOUT
238
235
  )
239
236
  end
240
237
  Telemetry.git_command_ms(Ext::Telemetry::Command::PACK_OBJECTS, duration_ms)
@@ -242,7 +239,7 @@ module Datadog
242
239
  basename
243
240
  rescue => e
244
241
  log_failure(e, "git generate packfiles")
245
- telemetry_track_error(e, Ext::Telemetry::Command::PACK_OBJECTS)
242
+ Telemetry.track_error(e, Ext::Telemetry::Command::PACK_OBJECTS)
246
243
  nil
247
244
  end
248
245
 
@@ -251,34 +248,31 @@ module Datadog
251
248
  res = false
252
249
 
253
250
  duration_ms = Core::Utils::Time.measure(:float_millisecond) do
254
- res = exec_git_command("git rev-parse --is-shallow-repository") == "true"
251
+ res = CLI.exec_git_command(["rev-parse", "--is-shallow-repository"]) == "true"
255
252
  end
256
253
  Telemetry.git_command_ms(Ext::Telemetry::Command::CHECK_SHALLOW, duration_ms)
257
254
 
258
255
  res
259
256
  rescue => e
260
257
  log_failure(e, "git shallow clone")
261
- telemetry_track_error(e, Ext::Telemetry::Command::CHECK_SHALLOW)
258
+ Telemetry.track_error(e, Ext::Telemetry::Command::CHECK_SHALLOW)
262
259
  false
263
260
  end
264
261
 
265
262
  def self.git_unshallow
266
263
  Telemetry.git_command(Ext::Telemetry::Command::UNSHALLOW)
264
+ # @type var res: String?
267
265
  res = nil
268
266
 
269
- unshallow_command =
270
- "git fetch " \
271
- "--shallow-since=\"1 month ago\" " \
272
- "--update-shallow " \
273
- "--filter=\"blob:none\" " \
274
- "--recurse-submodules=no " \
275
- "$(git config --default origin --get clone.defaultRemoteName)"
276
-
277
- unshallow_remotes = [
278
- "$(git rev-parse HEAD)",
279
- "$(git rev-parse --abbrev-ref --symbolic-full-name @{upstream})",
280
- nil
281
- ]
267
+ default_remote = CLI.exec_git_command(["config", "--default", "origin", "--get", "clone.defaultRemoteName"])&.strip
268
+ head_commit = git_commit_sha
269
+ upstream_branch = get_upstream_branch
270
+
271
+ # Build array of remotes to try, filtering out nil values
272
+ unshallow_remotes = []
273
+ unshallow_remotes << head_commit if head_commit
274
+ unshallow_remotes << upstream_branch if upstream_branch
275
+ unshallow_remotes << nil # Ensure the loop runs at least once, even if no valid remotes are available. This acts as a fallback mechanism.
282
276
 
283
277
  duration_ms = Core::Utils::Time.measure(:float_millisecond) do
284
278
  unshallow_remotes.each do |remote|
@@ -286,17 +280,27 @@ module Datadog
286
280
 
287
281
  res =
288
282
  begin
289
- exec_git_command(
290
- "#{unshallow_command} #{remote}"
291
- )
283
+ # @type var cmd: Array[String]
284
+ cmd = [
285
+ "fetch",
286
+ "--shallow-since=\"1 month ago\"",
287
+ "--update-shallow",
288
+ "--filter=blob:none",
289
+ "--recurse-submodules=no",
290
+ default_remote
291
+ ]
292
+ cmd << remote if remote
293
+
294
+ CLI.exec_git_command(cmd, timeout: CLI::UNSHALLOW_TIMEOUT)
292
295
  rescue => e
293
296
  log_failure(e, "git unshallow")
294
- telemetry_track_error(e, Ext::Telemetry::Command::UNSHALLOW)
297
+ Telemetry.track_error(e, Ext::Telemetry::Command::UNSHALLOW)
295
298
  unshallowing_errored = true
296
299
  nil
297
300
  end
298
301
 
299
- break unless unshallowing_errored
302
+ # If the command succeeded, break and return the result
303
+ break [] if res && !unshallowing_errored
300
304
  end
301
305
  end
302
306
 
@@ -304,71 +308,80 @@ module Datadog
304
308
  res
305
309
  end
306
310
 
307
- # makes .exec_git_command private to make sure that this method
308
- # is not called from outside of this module with insecure parameters
309
- class << self
310
- private
311
+ # Returns a Set of normalized file paths changed since the given base_commit.
312
+ # If base_commit is nil, returns nil. On error, returns nil.
313
+ def self.get_changed_files_from_diff(base_commit)
314
+ return nil if base_commit.nil?
311
315
 
312
- def filter_invalid_commits(commits)
313
- commits.filter { |commit| Utils::Git.valid_commit_sha?(commit) }
314
- end
316
+ Datadog.logger.debug { "calculating git diff from base_commit: #{base_commit}" }
315
317
 
316
- def exec_git_command(cmd, stdin: nil)
317
- # Shell injection is alleviated by making sure that no outside modules call this method.
318
- # It is called only internally with static parameters.
319
- # no-dd-sa:ruby-security/shell-injection
320
- out, status = Open3.capture2e(cmd, stdin_data: stdin)
321
-
322
- if status.nil?
323
- retry_count = COMMAND_RETRY_COUNT
324
- Datadog.logger.debug { "Opening pipe failed, starting retries..." }
325
- while status.nil? && retry_count.positive?
326
- # no-dd-sa:ruby-security/shell-injection
327
- out, status = Open3.capture2e(cmd, stdin_data: stdin)
328
- Datadog.logger.debug { "After retry status is [#{status}]" }
329
- retry_count -= 1
330
- end
331
- end
318
+ Telemetry.git_command(Ext::Telemetry::Command::DIFF)
332
319
 
333
- if status.nil? || !status.success?
334
- raise GitCommandExecutionError.new(
335
- "Failed to run git command [#{cmd}] with input [#{stdin}] and output [#{out}]",
336
- output: out,
337
- command: cmd,
338
- status: status
339
- )
340
- end
320
+ begin
321
+ # 1. Run the git diff command
341
322
 
342
- # Sometimes Encoding.default_external is somehow set to US-ASCII which breaks
343
- # commit messages with UTF-8 characters like emojis
344
- # We force output's encoding to be UTF-8 in this case
345
- # This is safe to do as UTF-8 is compatible with US-ASCII
346
- if Encoding.default_external == Encoding::US_ASCII
347
- out = out.force_encoding(Encoding::UTF_8)
323
+ # @type var output: String?
324
+ output = nil
325
+ duration_ms = Core::Utils::Time.measure(:float_millisecond) do
326
+ output = CLI.exec_git_command(["diff", "-U0", "--word-diff=porcelain", base_commit, "HEAD"], timeout: CLI::LONG_TIMEOUT)
348
327
  end
349
- out.strip! # There's always a "\n" at the end of the command output
328
+ Telemetry.git_command_ms(Ext::Telemetry::Command::DIFF, duration_ms)
329
+
330
+ Datadog.logger.debug { "git diff output: #{output}" }
331
+
332
+ return nil if output.nil?
333
+
334
+ # 2. Parse the output to extract which files changed
335
+ changed_files = Set.new
336
+ output.each_line do |line|
337
+ # Match lines like: diff --git a/foo/bar.rb b/foo/bar.rb
338
+ # This captures git changes on file level
339
+ match = /^diff --git a\/(?<file>.+?) b\//.match(line)
340
+ if match && match[:file]
341
+ changed_file = match[:file]
342
+ # Normalize to repo root
343
+ normalized_changed_file = relative_to_root(changed_file)
344
+ changed_files << normalized_changed_file unless normalized_changed_file.nil? || normalized_changed_file.empty?
345
+
346
+ Datadog.logger.debug { "matched changed_file: #{changed_file} from line: #{line}" }
347
+ Datadog.logger.debug { "normalized_changed_file: #{normalized_changed_file}" }
348
+ end
349
+ end
350
+ changed_files
351
+ rescue => e
352
+ Telemetry.track_error(e, Ext::Telemetry::Command::DIFF)
353
+ log_failure(e, "get changed files from diff")
354
+ nil
355
+ end
356
+ end
350
357
 
351
- return nil if out.empty?
358
+ # On best effort basis determines the git sha of the most likely
359
+ # base branch for the current PR.
360
+ def self.base_commit_sha(base_branch: nil)
361
+ Telemetry.git_command(Ext::Telemetry::Command::BASE_COMMIT_SHA)
352
362
 
353
- out
354
- end
363
+ BaseBranchShaDetector.base_branch_sha(base_branch)
364
+ rescue => e
365
+ Telemetry.track_error(e, Ext::Telemetry::Command::BASE_COMMIT_SHA)
366
+ log_failure(e, "git base ref")
367
+ nil
368
+ end
355
369
 
356
- def log_failure(e, action)
357
- Datadog.logger.debug(
358
- "Unable to perform #{action}: #{e.class.name} #{e.message} at #{Array(e.backtrace).first}"
359
- )
360
- end
370
+ def self.get_upstream_branch
371
+ CLI.exec_git_command(["rev-parse", "--abbrev-ref", "--symbolic-full-name", "@{upstream}"])
372
+ rescue => e
373
+ Datadog.logger.debug { "Error getting upstream: #{e}" }
374
+ nil
375
+ end
361
376
 
362
- def telemetry_track_error(e, command)
363
- case e
364
- when Errno::ENOENT
365
- Telemetry.git_command_errors(command, executable_missing: true)
366
- when GitCommandExecutionError
367
- Telemetry.git_command_errors(command, exit_code: e.status&.to_i)
368
- else
369
- Telemetry.git_command_errors(command, exit_code: -9000)
370
- end
371
- end
377
+ def self.filter_invalid_commits(commits)
378
+ commits.filter { |commit| Utils::Git.valid_commit_sha?(commit) }
379
+ end
380
+
381
+ def self.log_failure(e, action)
382
+ Datadog.logger.debug(
383
+ "Unable to perform #{action}: #{e.class.name} #{e.message} at #{Array(e.backtrace).first}"
384
+ )
372
385
  end
373
386
  end
374
387
  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
@@ -15,10 +15,11 @@ module Datadog
15
15
  module CI
16
16
  module Git
17
17
  class TreeUploader
18
- attr_reader :api
18
+ attr_reader :api, :force_unshallow
19
19
 
20
- def initialize(api:)
20
+ def initialize(api:, force_unshallow: false)
21
21
  @api = api
22
+ @force_unshallow = force_unshallow
22
23
  end
23
24
 
24
25
  def call(repository_url)
@@ -45,7 +46,13 @@ module Datadog
45
46
  # ask the backend for the list of commits it already has
46
47
  known_commits, new_commits = fetch_known_commits_and_split(repository_url, latest_commits)
47
48
  # if all commits are present in the backend, we don't need to upload anything
48
- if new_commits.empty?
49
+
50
+ # We optimize unshallowing process by checking the latest available commits with backend:
51
+ # if they are already known to backend, then we don't have to unshallow.
52
+ #
53
+ # Sometimes we need to unshallow anyway: for impacted tests detection feature for example we need
54
+ # to calculate git diffs locally. In this case we skip the optimization and always unshallow.
55
+ if new_commits.empty? && !@force_unshallow
49
56
  Datadog.logger.debug("No new commits to upload")
50
57
  return
51
58
  end
@@ -0,0 +1,81 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "set"
4
+
5
+ require_relative "../ext/test"
6
+ require_relative "../git/local_repository"
7
+
8
+ module Datadog
9
+ module CI
10
+ module ImpactedTestsDetection
11
+ class Component
12
+ def initialize(enabled:)
13
+ @enabled = enabled
14
+ @changed_files = Set.new
15
+ end
16
+
17
+ def configure(library_settings, test_session)
18
+ @enabled &&= library_settings.impacted_tests_enabled?
19
+
20
+ return unless @enabled
21
+
22
+ # we must unshallow the repository before trying to find base_commit_sha or executing `git diff` command
23
+ git_tree_upload_worker.wait_until_done
24
+
25
+ base_commit_sha = test_session.base_commit_sha || Git::LocalRepository.base_commit_sha
26
+ if base_commit_sha.nil?
27
+ Datadog.logger.debug { "Impacted tests detection disabled: base commit not found" }
28
+ @enabled = false
29
+ return
30
+ end
31
+
32
+ changed_files = Git::LocalRepository.get_changed_files_from_diff(base_commit_sha)
33
+ if changed_files.nil?
34
+ Datadog.logger.debug { "Impacted tests detection disabled: could not get changed files" }
35
+ @enabled = false
36
+ return
37
+ end
38
+
39
+ Datadog.logger.debug do
40
+ "Impacted tests detection: found #{changed_files.size} changed files"
41
+ end
42
+ Datadog.logger.debug do
43
+ "Impacted tests detection: changed files: #{changed_files.inspect}"
44
+ end
45
+
46
+ @changed_files = changed_files
47
+ @enabled = true
48
+ end
49
+
50
+ def enabled?
51
+ @enabled
52
+ end
53
+
54
+ def modified?(test_span)
55
+ return false unless enabled?
56
+
57
+ source_file = test_span.source_file
58
+ return false if source_file.nil?
59
+
60
+ @changed_files.include?(source_file)
61
+ end
62
+
63
+ def tag_modified_test(test_span)
64
+ return unless modified?(test_span)
65
+
66
+ Datadog.logger.debug do
67
+ "Impacted tests detection: test #{test_span.name} with source file #{test_span.source_file} is modified"
68
+ end
69
+
70
+ test_span.set_tag(Ext::Test::TAG_TEST_IS_MODIFIED, "true")
71
+ end
72
+
73
+ private
74
+
75
+ def git_tree_upload_worker
76
+ Datadog.send(:components).git_tree_upload_worker
77
+ end
78
+ end
79
+ end
80
+ end
81
+ end
@@ -46,7 +46,8 @@ module Datadog
46
46
  Worker.new { test_optimisation.configure(@library_configuration, test_session) },
47
47
  Worker.new { test_retries.configure(@library_configuration, test_session) },
48
48
  Worker.new { test_visibility.configure(@library_configuration, test_session) },
49
- Worker.new { test_management.configure(@library_configuration, test_session) }
49
+ Worker.new { test_management.configure(@library_configuration, test_session) },
50
+ Worker.new { impacted_tests_detection.configure(@library_configuration, test_session) }
50
51
  ]
51
52
 
52
53
  # launch configuration workers
@@ -89,6 +90,10 @@ module Datadog
89
90
  Datadog.send(:components).test_retries
90
91
  end
91
92
 
93
+ def impacted_tests_detection
94
+ Datadog.send(:components).impacted_tests_detection
95
+ end
96
+
92
97
  def git_tree_upload_worker
93
98
  Datadog.send(:components).git_tree_upload_worker
94
99
  end
@@ -106,6 +106,14 @@ module Datadog
106
106
  )
107
107
  end
108
108
 
109
+ def impacted_tests_enabled?
110
+ return @impacted_tests_enabled if defined?(@impacted_tests_enabled)
111
+
112
+ @impacted_tests_enabled = Utils::Parsing.convert_to_bool(
113
+ payload.fetch(Ext::Transport::DD_API_SETTINGS_RESPONSE_IMPACTED_TESTS_ENABLED_KEY, false)
114
+ )
115
+ end
116
+
109
117
  def slow_test_retries
110
118
  return @slow_test_retries if defined?(@slow_test_retries)
111
119
 
@@ -6,6 +6,7 @@ require "datadog/core/environment/platform"
6
6
 
7
7
  require_relative "ext/test"
8
8
  require_relative "utils/test_run"
9
+ require_relative "ext/git"
9
10
 
10
11
  module Datadog
11
12
  module CI
@@ -164,6 +165,12 @@ module Datadog
164
165
  tracer_span.get_tag(Ext::Git::TAG_BRANCH)
165
166
  end
166
167
 
168
+ # Returns the base commit SHA for the pull request, if available.
169
+ # @return [String, nil] the base commit SHA or nil if not set.
170
+ def base_commit_sha
171
+ tracer_span.get_tag(Ext::Git::TAG_PULL_REQUEST_BASE_BRANCH_SHA)
172
+ end
173
+
167
174
  # Returns the OS architecture extracted from the environment.
168
175
  # @return [String] OS arch.
169
176
  def os_architecture
@@ -102,6 +102,12 @@ module Datadog
102
102
  get_tag(Ext::Test::TAG_IS_ATTEMPT_TO_FIX) == "true"
103
103
  end
104
104
 
105
+ # Returns "true" if this test is marked as modified (e.g., impacted by code changes).
106
+ # @return [Boolean] true if this test is modified, false otherwise.
107
+ def modified?
108
+ get_tag(Ext::Test::TAG_TEST_IS_MODIFIED) == "true"
109
+ end
110
+
105
111
  # Marks this test as unskippable by the Test Impact Analysis.
106
112
  # This must be done before the test execution starts.
107
113
  #
@@ -118,7 +118,8 @@ module Datadog
118
118
  "type" => Ext::Transport::DD_API_TEST_MANAGEMENT_TESTS_TYPE,
119
119
  "attributes" => {
120
120
  "repository_url" => test_session.git_repository_url,
121
- "commit_message" => test_session.git_commit_message
121
+ "commit_message" => test_session.git_commit_message,
122
+ "sha" => test_session.git_commit_sha
122
123
  }
123
124
  }
124
125
  }.to_json