bitfab 0.68.0 → 0.69.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: 7c5044f5cc55988582b4a4428242ac2c0ea12fb87ba228054ad7c01a08e760ca
4
+ data.tar.gz: f410e0a0560c11fd3185c4f237bb30d37ac16ed5dcb3c0d4beb2d08d5b62f863
5
5
  SHA512:
6
- metadata.gz: 551641d9eb87d4c98d16d17cf5f6dcfe3f795f84a8fe1c155ba14d5db41bb5e53b04229ce0424ecac59430f854e79790c1460fb9fbc7ba10a2e0cb2bf739acd4
7
- data.tar.gz: 28c329c02e65d57a75af5ae12895be94bc525dfa6ef01e2203a7485e3998a5d6c0e04209e2e78694f6085777bdad2009dba36c6d8ee4b7e75bb95bf8d98e6f0f
6
+ metadata.gz: 680f7e7b023a94951496fba7b6b2aef5bb61f4250d3a3cfa5d2db3dae7a91038a0c3f03474419eb65e9defa0fc0c3811d7ee4346a4d9b50c15f2fa29a3bda703
7
+ data.tar.gz: b6230d97bb985ad5748e7e45121f89fdc4cbf65861eaa7ac815bd028bd34d43774a36f4ab3941b54e830121c7c097e2ee98da013834d783a69147a7b3a4dc570
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 = {}
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(
@@ -591,10 +594,6 @@ module Bitfab
591
594
  result
592
595
  end
593
596
 
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
597
  CC_MAX_FILES = 60
599
598
  CC_MAX_FILE_BYTES = 500_000
600
599
  CC_MAX_TOTAL_BYTES = 2_000_000
@@ -655,21 +654,36 @@ module Bitfab
655
654
  settings.empty? ? nil : settings
656
655
  end
657
656
 
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)
657
+ def validate_code_change(code_change, description, files)
658
+ if code_change.nil?
659
+ raise ArgumentError, "replay needs a code_change naming the code this run tests: #{CODE_CHANGE_CHOICES}."
660
+ end
661
+ unless CODE_CHANGE_TYPES.include?(code_change)
662
+ raise ArgumentError, "Unknown code_change #{code_change.inspect}. Pass #{CODE_CHANGE_CHOICES}."
663
+ end
664
+ if code_change == "manual" && files.nil?
665
+ raise ArgumentError,
666
+ 'code_change "manual" needs code_change_files: the path and before/after contents of each edited file.'
667
+ end
668
+ if code_change != "manual" && !files.nil?
669
+ raise ArgumentError, "code_change_files only applies with code_change \"manual\" (got \"#{code_change}\")."
670
+ end
671
+ if code_change == "skip" && !description.nil?
672
+ raise ArgumentError, 'code_change "skip" attaches no code change, so it cannot take a code_change_description.'
673
+ end
674
+ code_change
675
+ end
676
+
677
+ def resolve_code_change(code_change, description, files, label = nil)
678
+ return [nil, nil] if code_change == "skip"
679
+ return [description, files] if code_change == "manual"
680
+
681
+ captured = read_code_change_file || capture_code_change_from_git(Dir.pwd, code_change, label)
682
+ if captured.nil? || captured[1].nil? || captured[1].empty?
683
+ warn "Bitfab: code_change \"#{code_change}\" found no changed files, so this replay has no code change attached."
684
+ return [description, nil]
685
+ end
686
+ [description || captured[0], captured[1]]
673
687
  end
674
688
 
675
689
  def read_code_change_file
@@ -679,8 +693,6 @@ module Bitfab
679
693
  begin
680
694
  parsed = JSON.parse(File.read(path))
681
695
  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
696
  files = nil unless files.is_a?(Array) && files.all?(Hash)
685
697
  description = parsed["description"]
686
698
  description = nil unless description.is_a?(String)
@@ -700,18 +712,20 @@ module Bitfab
700
712
  !git(root, ["rev-parse", "--verify", "#{ref}^{object}"]).nil?
701
713
  end
702
714
 
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
715
+ def cc_resolve_base(root, type)
716
+ case type
717
+ when "last-branch" then cc_resolve_branch_point(root)
718
+ when "default-branch" then cc_resolve_default_branch_point(root)
719
+ else cc_ref_exists?(root, "HEAD") ? ["HEAD", "uncommitted (vs HEAD)"] : nil
713
720
  end
714
- cc_ref_exists?(root, "HEAD") ? ["HEAD", "uncommitted (vs HEAD)"] : nil
721
+ end
722
+
723
+ def cc_resolve_default_branch_point(root)
724
+ default = cc_default_branch(root)
725
+ return nil if default.nil?
726
+
727
+ base = (git(root, ["merge-base", "HEAD", default]) || "").strip
728
+ base.empty? ? nil : [base, "vs #{default}"]
715
729
  end
716
730
 
717
731
  def cc_list_changes(root, base)
@@ -794,22 +808,15 @@ module Bitfab
794
808
  0
795
809
  end
796
810
 
797
- def capture_code_change_from_git(cwd, label = nil)
811
+ def capture_code_change_from_git(cwd, type, label = nil)
798
812
  root = git(cwd, ["rev-parse", "--show-toplevel"])
799
813
  return nil if root.nil? || root.strip.empty?
800
814
 
801
815
  root = root.strip
802
- resolved = cc_resolve_base(root)
816
+ resolved = cc_resolve_base(root, type)
803
817
  return nil if resolved.nil?
804
818
 
805
819
  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
820
  return nil if entries.empty?
814
821
 
815
822
  base, against = resolved
@@ -819,15 +826,10 @@ module Bitfab
819
826
  entries.each do |status, before_path, path|
820
827
  break if files.length >= CC_MAX_FILES
821
828
 
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
829
  before_bytes = (status == "A") ? 0 : cc_blob_bytes(root, base, before_path)
825
830
  after_bytes = (status == "D") ? 0 : cc_working_bytes(root, path)
826
831
  next if before_bytes > CC_MAX_FILE_BYTES || after_bytes > CC_MAX_FILE_BYTES
827
832
 
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
833
  before = (status == "A") ? "" : (git(root, ["show", "#{base}:#{before_path}"]) || "").gsub("\r\n", "\n")
832
834
  after = (status == "D") ? "" : cc_read_working_file(root, path).gsub("\r\n", "\n")
833
835
  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.69.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.69.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Harvest Team