bitfab 0.68.0 → 0.70.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: c05530ec639d246b474f368fae48df4ed2230e4275272f2d903c9ef81bc96b6b
4
- data.tar.gz: 1019089f79f07cafb3b0e8be5604717ed32a1a894f28a7ae6b26d71cf05c1889
3
+ metadata.gz: 1db22eed10f5c7fd7e24777507be907e01a1a65f7df1a10ff388bc347e17b5f7
4
+ data.tar.gz: e30cd6c045153777120348af94e6884ee77fef98cb433e14c470886b6085842a
5
5
  SHA512:
6
- metadata.gz: 551641d9eb87d4c98d16d17cf5f6dcfe3f795f84a8fe1c155ba14d5db41bb5e53b04229ce0424ecac59430f854e79790c1460fb9fbc7ba10a2e0cb2bf739acd4
7
- data.tar.gz: 28c329c02e65d57a75af5ae12895be94bc525dfa6ef01e2203a7485e3998a5d6c0e04209e2e78694f6085777bdad2009dba36c6d8ee4b7e75bb95bf8d98e6f0f
6
+ metadata.gz: 838efe0f9650542d604b48c330aa1dbd033f96091dd7a24ad050d1b8929854cee0af10342d2a3882aaef97c33ab4fb492b1e8a3f1237e74bfdc0ac0a51546064
7
+ data.tar.gz: 6244270e97642c69cad5fc9c08bb26fa7492149d2d98f04ec6432c4c9f314ba577649f5af2d13913ba39704f0fca46c0d03d435845b647532f9068d97da323aa
data/lib/bitfab/client.rb CHANGED
@@ -140,15 +140,23 @@ module Bitfab
140
140
  # such as an environment override or a forced feature flag. Kept on the
141
141
  # experiment next to its name (max 10,000 characters).
142
142
  # @param max_concurrency [Integer, nil] max threads for parallel replay (default: 10)
143
- # @param code_change_description [String, nil] optional rationale for the
144
- # code change being tested in this replay (stored on the experiment).
145
- # When supplied without code_change_files, this text is preserved while
146
- # the SDK automatically captures the files.
147
- # Pass nil when the replay should have no description; use
148
- # code_change_files: nil to suppress automatic file capture.
149
- # @param code_change_files [Array<Hash>, nil] optional list of edited files,
150
- # each as { path:, before:, after: } (use "" for new/deleted files).
151
- # Omit to capture automatically, or pass nil to suppress file capture.
143
+ # @param code_change [String] required. Which code change this replay
144
+ # tests, stored on the experiment so it can be reviewed alongside the
145
+ # results. A replay without it raises ArgumentError before any request.
146
+ # "working-tree" is the uncommitted changes vs HEAD; "last-branch" the
147
+ # diff against the branch this one was cut from, such as the branch below
148
+ # it in a stack, else the default branch; "default-branch" the diff
149
+ # against where HEAD split from the default branch (origin/HEAD, else
150
+ # origin/main, origin/master, main, master); "skip" no code change, such
151
+ # as for a baseline run; "manual" the files in code_change_files. The
152
+ # three git choices include uncommitted edits and untracked files, and
153
+ # warn and attach nothing when they find no changed files.
154
+ # @param code_change_description [String, nil] description of the code
155
+ # change, stored on the experiment. Defaults to one generated from the
156
+ # captured files. Not accepted with "skip".
157
+ # @param code_change_files [Array<Hash>, nil] the edited files when
158
+ # code_change is "manual", and only then, each as { path:, before:, after: }
159
+ # (use "" for new/deleted files).
152
160
  # @param experiment_group_id [String, nil] optional UUID grouping multiple
153
161
  # replay runs into a single experiment batch. The run joins that group,
154
162
  # and an id no group has yet creates it
@@ -214,7 +222,7 @@ module Bitfab
214
222
  # @return [Hash] with :items, :experiment_id, :experiment_url, and the
215
223
  # deprecated aliases :test_run_id and :test_run_url carrying the same values
216
224
  def replay(receiver, method_name = nil, trace_function_key:, limit: nil, trace_ids: nil, concurrency: nil, max_concurrency: ReplayConcurrency::UNSET,
217
- name: nil, notes: nil, metadata: nil, code_change_description: Replay::CODE_CHANGE_UNSET, code_change_files: Replay::CODE_CHANGE_UNSET, experiment_group_id: nil, experiment_group_name: nil, dataset_id: nil, dataset_ids: nil, grader_ids: nil, mock: "marked", attempts: ReplayConcurrency::UNSET, only_with_assertions: false, skip_assertion_judging: false, judge_assertions: nil, dry_run: false,
225
+ name: nil, notes: nil, metadata: nil, code_change: nil, code_change_description: nil, code_change_files: nil, experiment_group_id: nil, experiment_group_name: nil, dataset_id: nil, dataset_ids: nil, grader_ids: nil, mock: "marked", attempts: ReplayConcurrency::UNSET, only_with_assertions: false, skip_assertion_judging: false, judge_assertions: nil, dry_run: false,
218
226
  adapt_inputs: nil, mock_override: nil, db_branch: nil, on_item_start: nil, on_item_finish: nil, on_progress: nil,
219
227
  on_experiment_start: nil, experimental_selective_replay: nil)
220
228
  Replay.run(
@@ -234,6 +242,7 @@ module Bitfab
234
242
  skip_assertion_judging:,
235
243
  judge_assertions:,
236
244
  dry_run:,
245
+ code_change:,
237
246
  code_change_description:,
238
247
  code_change_files:,
239
248
  experiment_group_id:,
@@ -147,8 +147,9 @@ RENAMED_FLAGS = {
147
147
  "--cloud-include": "nothing: new files that git does not ignore are already in the snapshot",
148
148
  }
149
149
  SEED_FLAGS = ("--seed", "--cases", "--from-trace", "--run")
150
- PATH_FLAGS = ("--registry", "--params", "--code-change")
151
- CODE_CHANGE_FLAGS = ("--code-change", "--no-code-change")
150
+ PATH_FLAGS = ("--registry", "--params", "--code-change-file")
151
+ CODE_CHANGE_FLAG = "--code-change"
152
+ BRANCH_CODE_CHANGES = ("last-branch", "default-branch")
152
153
  CHECKOUT_DEPTH = 2
153
154
  CODE_CHANGE_PATH_ENV = "BITFAB_CODE_CHANGE_PATH"
154
155
  CODE_CHANGE_MAX_FILES = 60
@@ -1022,6 +1023,21 @@ def branch_point(root):
1022
1023
  return None
1023
1024
 
1024
1025
 
1026
+ def default_branch_point(root):
1027
+ default = default_branch(root)
1028
+ sha = quiet_git(root, "merge-base", "HEAD", default) if default else ""
1029
+ return {"sha": sha, "ref": default} if sha else None
1030
+
1031
+
1032
+ def code_change_base(root, replay):
1033
+ kind = flag_value(replay, CODE_CHANGE_FLAG)
1034
+ if kind == "last-branch":
1035
+ return branch_point(root)
1036
+ if kind == "default-branch":
1037
+ return default_branch_point(root)
1038
+ return None
1039
+
1040
+
1025
1041
  def replay_artifact(path):
1026
1042
  return path == ".bitfab" or path.startswith(".bitfab/")
1027
1043
 
@@ -1126,11 +1142,10 @@ def snapshot(
1126
1142
  }
1127
1143
  if preview:
1128
1144
  return summary
1129
- code_change_base = request.get("codeChangeBase") or {}
1145
+ branch_base = str((request.get("codeChangeBase") or {}).get("sha", ""))
1130
1146
  extra_parent = (
1131
- ["-p", code_change_base["sha"]]
1132
- if not any(not replay_artifact(path) for _, path in changes)
1133
- and SHA.fullmatch(str(code_change_base.get("sha", "")))
1147
+ ["-p", branch_base]
1148
+ if SHA.fullmatch(branch_base) and branch_base != head
1134
1149
  else []
1135
1150
  )
1136
1151
  sha = git(
@@ -1461,9 +1476,9 @@ def submit(root, repo, workflow, parsed, path):
1461
1476
  branch = current_branch(root)
1462
1477
  if branch:
1463
1478
  request["branch"] = branch
1464
- code_change_base = branch_point(root)
1465
- if code_change_base:
1466
- request["codeChangeBase"] = code_change_base
1479
+ base = code_change_base(root, parsed["replay"])
1480
+ if base:
1481
+ request["codeChangeBase"] = base
1467
1482
  check_arguments(parsed["replay"])
1468
1483
  allowed = allowed_files(root, parsed)
1469
1484
  if "--cloud-preview" in parsed["switches"]:
@@ -2121,15 +2136,19 @@ def check_out_snapshot_base(root, commit, branch):
2121
2136
  return base
2122
2137
 
2123
2138
 
2124
- def replay_name(args):
2139
+ def flag_value(args, flag):
2125
2140
  for index, value in enumerate(args):
2126
- if value.startswith("--name="):
2141
+ if value.startswith(f"{flag}="):
2127
2142
  return value.partition("=")[2]
2128
- if value == "--name" and index + 1 < len(args):
2143
+ if value == flag and index + 1 < len(args):
2129
2144
  return args[index + 1]
2130
2145
  return None
2131
2146
 
2132
2147
 
2148
+ def replay_name(args):
2149
+ return flag_value(args, "--name")
2150
+
2151
+
2133
2152
  def git_blob(root, ref, path):
2134
2153
  try:
2135
2154
  size = int(git(root, "cat-file", "-s", f"{ref}:{path}"))
@@ -2209,7 +2228,10 @@ def branch_code_change(root, base, commit, request):
2209
2228
  and SHA.fullmatch(sha)
2210
2229
  and isinstance(ref, str)
2211
2230
  and BRANCH_NAME.fullmatch(ref)
2212
- and quiet_git(root, "rev-parse", "--verify", "-q", f"{commit}^2") == sha
2231
+ and (
2232
+ sha == base
2233
+ or quiet_git(root, "rev-parse", "--verify", "-q", f"{commit}^2") == sha
2234
+ )
2213
2235
  ):
2214
2236
  return None
2215
2237
  return snapshot_code_change(
@@ -2217,6 +2239,17 @@ def branch_code_change(root, base, commit, request):
2217
2239
  )
2218
2240
 
2219
2241
 
2242
+ def runner_code_change(root, base, commit, request):
2243
+ kind = flag_value(request["args"], CODE_CHANGE_FLAG)
2244
+ if kind == "working-tree":
2245
+ change = snapshot_code_change(root, base, commit, replay_name(request["args"]))
2246
+ elif kind in BRANCH_CODE_CHANGES:
2247
+ change = branch_code_change(root, base, commit, request)
2248
+ else:
2249
+ return None
2250
+ return change or {"files": []}
2251
+
2252
+
2220
2253
  def write_code_change(change):
2221
2254
  handle, path = tempfile.mkstemp(
2222
2255
  prefix="bitfab-code-change-",
@@ -2247,22 +2280,11 @@ def execute():
2247
2280
  if environment:
2248
2281
  workflow_environment_conflicts(root, environment)
2249
2282
  print(f"Setting {', '.join(sorted(environment))} from the request", flush=True)
2250
- names = {option_name(value) for value in request["args"]}
2251
- explicit_change = bool(names & set(CODE_CHANGE_FLAGS))
2252
- change = (
2253
- None
2254
- if explicit_change or base is None
2255
- else snapshot_code_change(root, base, commit, replay_name(request["args"]))
2256
- or branch_code_change(root, base, commit, request)
2257
- )
2283
+ change = None if base is None else runner_code_change(root, base, commit, request)
2258
2284
  run_environment = dict(environment)
2259
- if change:
2285
+ if change is not None:
2260
2286
  run_environment[CODE_CHANGE_PATH_ENV] = write_code_change(change)
2261
- args = [
2262
- *replay_command(),
2263
- *request["args"],
2264
- *([] if explicit_change or change else ["--no-code-change"]),
2265
- ]
2287
+ args = [*replay_command(), *request["args"]]
2266
2288
  if dry_run(request["args"]):
2267
2289
  run_check_command(directory, environment)
2268
2290
  experiment = {}
@@ -253,6 +253,9 @@ module Bitfab
253
253
  # Start a replay session by fetching historical traces.
254
254
  # Blocking call. Returns hash with experimentId, experimentUrl, and items array.
255
255
  #
256
+ # @param code_change_type [String, nil] the code change choice this replay
257
+ # named, such as "default-branch" or "skip"; stored on the experiment so
258
+ # the dashboard can say what the run was compared against
256
259
  # @param code_change_description [String, nil] optional rationale for the
257
260
  # code change being tested in this replay
258
261
  # @param code_change_files [Array<Hash>, nil] optional list of edited files,
@@ -273,7 +276,7 @@ module Bitfab
273
276
  # directly to this experiment, graded as the union with the dataset's
274
277
  # runnable graders at completion; each must be an active/live grader in the
275
278
  # same org and trace function or the server rejects the replay
276
- def start_replay(trace_function_key, limit, trace_ids: nil, code_change_description: nil,
279
+ def start_replay(trace_function_key, limit, trace_ids: nil, code_change_type: nil, code_change_description: nil,
277
280
  code_change_files: nil, experiment_group_id: nil, experiment_group_name: nil, name: nil, notes: nil, metadata: nil, include_db_branch_lease: false, dataset_ids: nil,
278
281
  grader_ids: nil, db_branch_settings: nil, attempts: 1, only_with_assertions: false, skip_assertion_judging: false, include_original_metadata: false, experimental_selective_replay: nil, dry_run: false, invocation: nil)
279
282
  payload = {
@@ -286,6 +289,7 @@ module Bitfab
286
289
  payload["name"] = name unless name.nil?
287
290
  payload["notes"] = notes unless notes.nil?
288
291
  payload["metadata"] = metadata unless metadata.nil?
292
+ payload["codeChangeType"] = code_change_type unless code_change_type.nil?
289
293
  payload["codeChangeDescription"] = code_change_description unless code_change_description.nil?
290
294
  payload["codeChangeFiles"] = normalize_code_change_files(code_change_files) unless code_change_files.nil?
291
295
  payload["experimentGroupId"] = experiment_group_id unless experiment_group_id.nil?
data/lib/bitfab/replay.rb CHANGED
@@ -142,7 +142,12 @@ module Bitfab
142
142
 
143
143
  # Replay historical traces through a traced method and create an experiment.
144
144
  module Replay
145
- CODE_CHANGE_UNSET = Object.new.freeze
145
+ # The values Client#replay accepts for +code_change:+.
146
+ CODE_CHANGE_TYPES = %w[working-tree last-branch default-branch skip manual].freeze
147
+ # The +code_change:+ choices, as named in the errors that refuse a replay.
148
+ CODE_CHANGE_CHOICES = '"working-tree" (uncommitted changes vs HEAD), "last-branch" (the diff against the ' \
149
+ 'branch this one was cut from), "default-branch" (the diff against the default branch), "skip" (no code ' \
150
+ 'change), or "manual" (pass the files yourself)'
146
151
  UNSET_DEFAULT = Object.new.freeze
147
152
  OPTION_DEFAULTS = {mock: "marked"}.freeze
148
153
  PERSISTENCE_TIMEOUT_SECONDS = 30.0
@@ -199,7 +204,7 @@ module Bitfab
199
204
  def explicit_replay_options(options)
200
205
  options.reject do |option, value|
201
206
  value.nil? || value == false || value.equal?(ReplayConcurrency::UNSET) ||
202
- value.equal?(CODE_CHANGE_UNSET) || value == OPTION_DEFAULTS.fetch(option, UNSET_DEFAULT)
207
+ value == OPTION_DEFAULTS.fetch(option, UNSET_DEFAULT)
203
208
  end
204
209
  end
205
210
 
@@ -225,15 +230,23 @@ module Bitfab
225
230
  # such as an environment override or a forced feature flag. Kept on the
226
231
  # experiment next to its name (max 10,000 characters).
227
232
  # @param max_concurrency [Integer, nil] max threads for parallel replay (default: 10)
228
- # @param code_change_description [String, nil] optional rationale for the
229
- # code change being tested in this replay (stored on the experiment).
230
- # When supplied without code_change_files, this text is preserved while
231
- # the SDK automatically captures the files.
232
- # Pass nil when the replay should have no description; use
233
- # code_change_files: nil to suppress automatic file capture.
234
- # @param code_change_files [Array<Hash>, nil] optional list of edited files,
235
- # each as { path:, before:, after: } (empty string for new/deleted files).
236
- # Omit to capture automatically, or pass nil to suppress file capture.
233
+ # @param code_change [String] required. Which code change this replay
234
+ # tests, stored on the experiment so it can be reviewed alongside the
235
+ # results. A replay without it raises ArgumentError before any request.
236
+ # "working-tree" is the uncommitted changes vs HEAD; "last-branch" the
237
+ # diff against the branch this one was cut from, such as the branch below
238
+ # it in a stack, else the default branch; "default-branch" the diff
239
+ # against where HEAD split from the default branch (origin/HEAD, else
240
+ # origin/main, origin/master, main, master); "skip" no code change, such
241
+ # as for a baseline run; "manual" the files in code_change_files. The
242
+ # three git choices include uncommitted edits and untracked files, and
243
+ # warn and attach nothing when they find no changed files.
244
+ # @param code_change_description [String, nil] description of the code
245
+ # change, stored on the experiment. Defaults to one generated from the
246
+ # captured files. Not accepted with "skip".
247
+ # @param code_change_files [Array<Hash>, nil] the edited files when
248
+ # code_change is "manual", and only then, each as { path:, before:, after: }
249
+ # (empty string for new/deleted files).
237
250
  # @param experiment_group_id [String, nil] optional UUID grouping multiple
238
251
  # replay runs into a single experiment batch. The run joins that group,
239
252
  # and an id no group has yet creates it
@@ -300,10 +313,11 @@ module Bitfab
300
313
  # The experiment fields are nil for a dry run, which creates no experiment,
301
314
  # and progress events then carry a nil :experiment_id too.
302
315
  def run(client, receiver, method_name, trace_function_key:, limit: nil, trace_ids: nil, name: nil, notes: nil, metadata: nil,
303
- concurrency: nil, max_concurrency: ReplayConcurrency::UNSET, code_change_description: CODE_CHANGE_UNSET, code_change_files: CODE_CHANGE_UNSET, experiment_group_id: nil, experiment_group_name: nil,
316
+ concurrency: nil, max_concurrency: ReplayConcurrency::UNSET, code_change: nil, code_change_description: nil, code_change_files: nil, experiment_group_id: nil, experiment_group_name: nil,
304
317
  dataset_id: nil, dataset_ids: nil, grader_ids: nil, mock: "marked", attempts: ReplayConcurrency::UNSET, only_with_assertions: false, skip_assertion_judging: false, judge_assertions: nil, dry_run: false,
305
318
  adapt_inputs: nil, mock_override: nil, db_branch: nil, on_item_start: nil, on_item_finish: nil, on_progress: nil,
306
319
  on_experiment_start: nil, experimental_selective_replay: nil)
320
+ validate_code_change(code_change, code_change_description, code_change_files)
307
321
  explicit_options = method(:run).parameters.filter_map { |type, option| [option, binding.local_variable_get(option)] if type == :key }
308
322
  skip_assertion_judging = resolve_skip_assertion_judging(skip_assertion_judging, judge_assertions)
309
323
  concurrency_options = ReplayConcurrency.resolve(concurrency, attempts, max_concurrency)
@@ -382,18 +396,7 @@ module Bitfab
382
396
 
383
397
  include_db_branch_lease = db_branch_enabled?(db_branch) && !dry_run
384
398
 
385
- # code_change_files controls capture: omitted auto-captures, an array
386
- # wins, and explicit nil opts out. Preserve a caller-supplied description
387
- # while filling only the omitted files.
388
- if code_change_files.equal?(CODE_CHANGE_UNSET)
389
- captured = resolve_auto_code_change(name)
390
- unless captured.nil?
391
- code_change_files = captured[1]
392
- code_change_description = captured[0] if code_change_description.equal?(CODE_CHANGE_UNSET)
393
- end
394
- end
395
- code_change_description = nil if code_change_description.equal?(CODE_CHANGE_UNSET)
396
- code_change_files = nil if code_change_files.equal?(CODE_CHANGE_UNSET)
399
+ code_change_description, code_change_files = resolve_code_change(code_change, code_change_description, code_change_files, name)
397
400
 
398
401
  resolved_db_branch_settings = db_branch_settings(db_branch)
399
402
  replay_data = http_client.start_replay(
@@ -403,6 +406,7 @@ module Bitfab
403
406
  name:,
404
407
  notes:,
405
408
  metadata:,
409
+ code_change_type: code_change,
406
410
  code_change_description:,
407
411
  code_change_files:,
408
412
  experiment_group_id:,
@@ -591,10 +595,6 @@ module Bitfab
591
595
  result
592
596
  end
593
597
 
594
- # Read the code-change payload the replay wrapper captured and pointed us at
595
- # via BITFAB_CODE_CHANGE_PATH. Returns [description, files] or nil.
596
- # Best-effort: any error (no env var, missing file, bad JSON) yields nil so
597
- # the replay proceeds with no code change rather than raising.
598
598
  CC_MAX_FILES = 60
599
599
  CC_MAX_FILE_BYTES = 500_000
600
600
  CC_MAX_TOTAL_BYTES = 2_000_000
@@ -655,21 +655,36 @@ module Bitfab
655
655
  settings.empty? ? nil : settings
656
656
  end
657
657
 
658
- # Auto-capture the code change to attach when the caller passed none. Lives
659
- # in replay() (the one call guaranteed to run on every replay) so it works no
660
- # matter how the replay was launched. Precedence: a BITFAB_CODE_CHANGE_PATH
661
- # override, else the git diff of the working tree vs HEAD (or vs the
662
- # merge-base with BITFAB_CODE_CHANGE_BASE). A clean working tree falls back to
663
- # the point where HEAD split from the default branch, so committed branch work
664
- # still shows. Best-effort: any failure yields nil.
665
- # An explicit code_change_files always wins over this.
666
- def resolve_auto_code_change(label = nil)
667
- return nil if ENV["BITFAB_DISABLE_CODE_CHANGE_CAPTURE"]
668
-
669
- from_env = read_code_change_file
670
- return from_env unless from_env.nil?
671
-
672
- capture_code_change_from_git(Dir.pwd, label)
658
+ def validate_code_change(code_change, description, files)
659
+ if code_change.nil?
660
+ raise ArgumentError, "replay needs a code_change naming the code this run tests: #{CODE_CHANGE_CHOICES}."
661
+ end
662
+ unless CODE_CHANGE_TYPES.include?(code_change)
663
+ raise ArgumentError, "Unknown code_change #{code_change.inspect}. Pass #{CODE_CHANGE_CHOICES}."
664
+ end
665
+ if code_change == "manual" && files.nil?
666
+ raise ArgumentError,
667
+ 'code_change "manual" needs code_change_files: the path and before/after contents of each edited file.'
668
+ end
669
+ if code_change != "manual" && !files.nil?
670
+ raise ArgumentError, "code_change_files only applies with code_change \"manual\" (got \"#{code_change}\")."
671
+ end
672
+ if code_change == "skip" && !description.nil?
673
+ raise ArgumentError, 'code_change "skip" attaches no code change, so it cannot take a code_change_description.'
674
+ end
675
+ code_change
676
+ end
677
+
678
+ def resolve_code_change(code_change, description, files, label = nil)
679
+ return [nil, nil] if code_change == "skip"
680
+ return [description, files] if code_change == "manual"
681
+
682
+ captured = read_code_change_file || capture_code_change_from_git(Dir.pwd, code_change, label)
683
+ if captured.nil? || captured[1].nil? || captured[1].empty?
684
+ warn "Bitfab: code_change \"#{code_change}\" found no changed files, so this replay has no code change attached."
685
+ return [description, nil]
686
+ end
687
+ [description || captured[0], captured[1]]
673
688
  end
674
689
 
675
690
  def read_code_change_file
@@ -679,8 +694,6 @@ module Bitfab
679
694
  begin
680
695
  parsed = JSON.parse(File.read(path))
681
696
  files = parsed["files"]
682
- # A malformed payload (non-array, or entries that aren't hashes) must
683
- # yield no code change, never crash normalize_code_change_files.
684
697
  files = nil unless files.is_a?(Array) && files.all?(Hash)
685
698
  description = parsed["description"]
686
699
  description = nil unless description.is_a?(String)
@@ -700,18 +713,20 @@ module Bitfab
700
713
  !git(root, ["rev-parse", "--verify", "#{ref}^{object}"]).nil?
701
714
  end
702
715
 
703
- # Returns [base, against]. HEAD by default, so only uncommitted edits show;
704
- # BITFAB_CODE_CHANGE_BASE moves the base to the merge-base with that ref.
705
- def cc_resolve_base(root)
706
- forced = ENV["BITFAB_CODE_CHANGE_BASE"]
707
- if forced && cc_ref_exists?(root, forced)
708
- mb = git(root, ["merge-base", "HEAD", forced])
709
- return [mb.strip, "vs #{forced}"] if mb && !mb.strip.empty?
710
-
711
- rp = git(root, ["rev-parse", "--verify", forced])
712
- return (rp && !rp.strip.empty?) ? [rp.strip, "vs #{forced}"] : nil
716
+ def cc_resolve_base(root, type)
717
+ case type
718
+ when "last-branch" then cc_resolve_branch_point(root)
719
+ when "default-branch" then cc_resolve_default_branch_point(root)
720
+ else cc_ref_exists?(root, "HEAD") ? ["HEAD", "uncommitted (vs HEAD)"] : nil
713
721
  end
714
- cc_ref_exists?(root, "HEAD") ? ["HEAD", "uncommitted (vs HEAD)"] : nil
722
+ end
723
+
724
+ def cc_resolve_default_branch_point(root)
725
+ default = cc_default_branch(root)
726
+ return nil if default.nil?
727
+
728
+ base = (git(root, ["merge-base", "HEAD", default]) || "").strip
729
+ base.empty? ? nil : [base, "vs #{default}"]
715
730
  end
716
731
 
717
732
  def cc_list_changes(root, base)
@@ -794,22 +809,15 @@ module Bitfab
794
809
  0
795
810
  end
796
811
 
797
- def capture_code_change_from_git(cwd, label = nil)
812
+ def capture_code_change_from_git(cwd, type, label = nil)
798
813
  root = git(cwd, ["rev-parse", "--show-toplevel"])
799
814
  return nil if root.nil? || root.strip.empty?
800
815
 
801
816
  root = root.strip
802
- resolved = cc_resolve_base(root)
817
+ resolved = cc_resolve_base(root, type)
803
818
  return nil if resolved.nil?
804
819
 
805
820
  entries = cc_list_changes(root, resolved[0])
806
- if entries.empty? && resolved[0] == "HEAD"
807
- branch_point = cc_resolve_branch_point(root)
808
- if branch_point
809
- resolved = branch_point
810
- entries = cc_list_changes(root, branch_point[0])
811
- end
812
- end
813
821
  return nil if entries.empty?
814
822
 
815
823
  base, against = resolved
@@ -819,15 +827,10 @@ module Bitfab
819
827
  entries.each do |status, before_path, path|
820
828
  break if files.length >= CC_MAX_FILES
821
829
 
822
- # Skip oversized files by size BEFORE reading their full contents, so a
823
- # huge changed file can't OOM/stall replay just to be discarded.
824
830
  before_bytes = (status == "A") ? 0 : cc_blob_bytes(root, base, before_path)
825
831
  after_bytes = (status == "D") ? 0 : cc_working_bytes(root, path)
826
832
  next if before_bytes > CC_MAX_FILE_BYTES || after_bytes > CC_MAX_FILE_BYTES
827
833
 
828
- # Normalize CRLF -> LF on both sides: git's blob is already LF-normalized
829
- # (autocrlf clean), so a raw CRLF working file would otherwise skew every
830
- # line as changed. Comparing/storing LF keeps the diff meaningful.
831
834
  before = (status == "A") ? "" : (git(root, ["show", "#{base}:#{before_path}"]) || "").gsub("\r\n", "\n")
832
835
  after = (status == "D") ? "" : cc_read_working_file(root, path).gsub("\r\n", "\n")
833
836
  next if before == after
@@ -47,7 +47,8 @@ module Bitfab
47
47
 
48
48
  def check_arguments(argv)
49
49
  _, replay_argv = parse(argv)
50
- ReplayCli.parse(nil, replay_argv)
50
+ args = ReplayCli.parse(nil, replay_argv)
51
+ ReplayCli.require_code_change(args) unless args[:seed] || args[:from_trace]
51
52
  0
52
53
  end
53
54
 
@@ -19,8 +19,7 @@ module Bitfab
19
19
  # Project-owned registry of production replay roots.
20
20
  class ReplayRegistry
21
21
  OPTION_NAMES = %i[
22
- limit trace_ids name notes metadata concurrency max_concurrency code_change_description
23
- code_change_files experiment_group_id experiment_group_name dataset_id dataset_ids grader_ids mock
22
+ limit trace_ids name notes metadata concurrency max_concurrency experiment_group_id experiment_group_name dataset_id dataset_ids grader_ids mock
24
23
  mock_override adapt_inputs db_branch attempts only_with_assertions skip_assertion_judging judge_assertions dry_run on_item_finish
25
24
  ].freeze
26
25
 
@@ -141,6 +140,7 @@ module Bitfab
141
140
  def run(registry, argv: ARGV, stdout: $stdout, stderr: $stderr)
142
141
  args = parse(registry, argv.dup)
143
142
  return SeedCli.run(registry, argv:, stdout:, stderr:) if args[:seed] || args[:from_trace]
143
+ require_code_change(args)
144
144
  stderr.puts "Bitfab: #{Replay::JUDGE_ASSERTIONS_DEPRECATION}" if args[:judge_assertions]
145
145
 
146
146
  registration = registry.fetch(args.fetch(:pipeline))
@@ -203,13 +203,11 @@ module Bitfab
203
203
  end
204
204
  end
205
205
 
206
- if args[:code_change]
207
- code_change = load_code_change(args[:code_change])
206
+ options[:code_change] = args.fetch(:code_change)
207
+ if args[:code_change_file]
208
+ code_change = load_code_change(args[:code_change_file])
208
209
  options[:code_change_description] = code_change.fetch("description")
209
210
  options[:code_change_files] = code_change.fetch("files")
210
- elsif args[:no_code_change]
211
- options[:code_change_description] = nil
212
- options[:code_change_files] = nil
213
211
  end
214
212
 
215
213
  if options[:concurrency] || args[:primitive] || args.key?(:memory_throttle)
@@ -305,6 +303,7 @@ module Bitfab
305
303
  if argv.include?("--db-branch") && argv.include?("--no-db-branch")
306
304
  raise OptionParser::InvalidArgument, "--db-branch and --no-db-branch cannot be used together"
307
305
  end
306
+ raise ArgumentError, "--no-code-change was replaced by --code-change skip." if argv.include?("--no-code-change")
308
307
  args = {}
309
308
  parser = OptionParser.new do |options|
310
309
  options.banner = "Usage: bitfab-replay <#{registry ? registry.names.join("|") : "pipeline"}> [options]"
@@ -338,7 +337,19 @@ module Bitfab
338
337
  options.on("--max-concurrency N", Integer) do |value|
339
338
  args[:max_concurrency] = positive_integer("--max-concurrency", value)
340
339
  end
341
- options.on("--code-change PATH") { |value| args[:code_change] = value }
340
+ options.on("--code-change working-tree|last-branch|default-branch|skip|manual",
341
+ "Required: the code change this run tests; working-tree is the uncommitted changes vs HEAD, " \
342
+ "last-branch the diff against the branch this one was cut from, default-branch the diff against the " \
343
+ "default branch, skip none, manual the edits in --code-change-file.") do |value|
344
+ unless Replay::CODE_CHANGE_TYPES.include?(value)
345
+ raise ArgumentError,
346
+ "Unknown --code-change '#{value}'. Pass #{Replay::CODE_CHANGE_CHOICES}. " \
347
+ "A file of edits goes in --code-change manual --code-change-file PATH."
348
+ end
349
+ args[:code_change] = value
350
+ end
351
+ options.on("--code-change-file PATH",
352
+ "With --code-change manual: a JSON file of { description, files }.") { |value| args[:code_change_file] = value }
342
353
  options.on("--experiment-group-id UUID",
343
354
  "Add this run to an experiment group. A UUID no group has yet creates the group.") { |value| args[:experiment_group_id] = value }
344
355
  options.on("--experiment-group-name NAME",
@@ -360,7 +371,6 @@ module Bitfab
360
371
  options.on("--mock STRATEGY", %w[none all marked]) { |value| args[:mock] = value }
361
372
  options.on("--db-branch") { args[:db_branch] = true }
362
373
  options.on("--no-db-branch") { args[:db_branch] = false }
363
- options.on("--no-code-change") { args[:no_code_change] = true }
364
374
  options.on("--params PATH") { |value| args[:params] = value }
365
375
  options.on("--param NAME=VALUE") { |value| (args[:param] ||= []) << value }
366
376
  end
@@ -369,9 +379,6 @@ module Bitfab
369
379
  raise OptionParser::InvalidArgument,
370
380
  "--trace-ids and --dataset-ids select different replay sources and cannot be used together"
371
381
  end
372
- if args[:code_change] && args[:no_code_change]
373
- raise OptionParser::InvalidArgument, "--code-change and --no-code-change cannot be used together"
374
- end
375
382
  pipeline = argv.shift
376
383
  known = pipeline && (registry.nil? || registry.names.include?(pipeline))
377
384
  raise OptionParser::MissingArgument, parser.banner unless known
@@ -380,6 +387,17 @@ module Bitfab
380
387
  args.merge(pipeline:)
381
388
  end
382
389
 
390
+ def require_code_change(args)
391
+ if args[:code_change].nil?
392
+ raise ArgumentError,
393
+ "--code-change is required: #{Replay::CODE_CHANGE_CHOICES}. \"manual\" takes the edits from --code-change-file PATH."
394
+ end
395
+ return args if (args[:code_change] == "manual") == !args[:code_change_file].nil?
396
+
397
+ raise ArgumentError,
398
+ "--code-change-file goes with --code-change manual, and --code-change manual needs --code-change-file PATH."
399
+ end
400
+
383
401
  def registry_dataset_ids(options)
384
402
  dataset_id = options[:dataset_id]
385
403
  dataset_ids = options[:dataset_ids]
@@ -416,7 +434,7 @@ module Bitfab
416
434
  def load_code_change(path)
417
435
  value = JSON.parse(File.read(path))
418
436
  unless value.is_a?(Hash) && value["description"].is_a?(String) && value["files"].is_a?(Array)
419
- raise ArgumentError, "Invalid --code-change file '#{path}': expected { description, files }."
437
+ raise ArgumentError, "Invalid --code-change-file '#{path}': expected { description, files }."
420
438
  end
421
439
 
422
440
  value
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Bitfab
4
- VERSION = "0.68.0"
4
+ VERSION = "0.70.0"
5
5
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: bitfab
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.68.0
4
+ version: 0.70.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Harvest Team