bitfab 0.69.0 → 0.71.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: 7c5044f5cc55988582b4a4428242ac2c0ea12fb87ba228054ad7c01a08e760ca
4
- data.tar.gz: f410e0a0560c11fd3185c4f237bb30d37ac16ed5dcb3c0d4beb2d08d5b62f863
3
+ metadata.gz: a84477fef0e70e513ab93e7ef81a0ef9021d5f09adefe8021075d89fbb26a64d
4
+ data.tar.gz: 39795db6575ecc859004b3db5af4d35b13747234d8fafd43f444819c4e2425dc
5
5
  SHA512:
6
- metadata.gz: 680f7e7b023a94951496fba7b6b2aef5bb61f4250d3a3cfa5d2db3dae7a91038a0c3f03474419eb65e9defa0fc0c3811d7ee4346a4d9b50c15f2fa29a3bda703
7
- data.tar.gz: b6230d97bb985ad5748e7e45121f89fdc4cbf65861eaa7ac815bd028bd34d43774a36f4ab3941b54e830121c7c097e2ee98da013834d783a69147a7b3a4dc570
6
+ metadata.gz: 0327c9092bb7b6d9071546c6a54ae5f64e49083518c6e35c3049eb04455b32eed4fa976df0c35a7d59d9231d0107cff212eca5a8287f5d4d043d1e566b5e1a58
7
+ data.tar.gz: bab39d209b61f795eb681bafdf7e5e19b6707977864e6c7a5b72d5ee02af449a8c1f0a3b8c29a06229ef32f099326dfe3c6ba1890156cdf9a37308daab137bf9
data/lib/bitfab/client.rb CHANGED
@@ -147,16 +147,22 @@ module Bitfab
147
147
  # diff against the branch this one was cut from, such as the branch below
148
148
  # it in a stack, else the default branch; "default-branch" the diff
149
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.
150
+ # origin/main, origin/master, main, master); "ref" the diff against where
151
+ # HEAD split from the branch or commit in code_change_base; "skip" no code
152
+ # change, such as for a baseline run; "manual" the files in
153
+ # code_change_files. The four git choices include uncommitted edits and
154
+ # untracked files, and warn and attach nothing when they find no changed
155
+ # files.
154
156
  # @param code_change_description [String, nil] description of the code
155
157
  # change, stored on the experiment. Defaults to one generated from the
156
158
  # captured files. Not accepted with "skip".
157
159
  # @param code_change_files [Array<Hash>, nil] the edited files when
158
160
  # code_change is "manual", and only then, each as { path:, before:, after: }
159
161
  # (use "" for new/deleted files).
162
+ # @param code_change_base [String, nil] the branch or commit to diff
163
+ # against when code_change is "ref", and only then, such as "dev",
164
+ # "release/2.3", or a commit SHA. A branch that exists only on the remote
165
+ # is read as origin/<name>. A name git cannot resolve raises ArgumentError.
160
166
  # @param experiment_group_id [String, nil] optional UUID grouping multiple
161
167
  # replay runs into a single experiment batch. The run joins that group,
162
168
  # and an id no group has yet creates it
@@ -222,7 +228,7 @@ module Bitfab
222
228
  # @return [Hash] with :items, :experiment_id, :experiment_url, and the
223
229
  # deprecated aliases :test_run_id and :test_run_url carrying the same values
224
230
  def replay(receiver, method_name = nil, trace_function_key:, limit: nil, trace_ids: nil, concurrency: nil, max_concurrency: ReplayConcurrency::UNSET,
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,
231
+ name: nil, notes: nil, metadata: nil, code_change: nil, code_change_description: nil, code_change_files: nil, code_change_base: 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,
226
232
  adapt_inputs: nil, mock_override: nil, db_branch: nil, on_item_start: nil, on_item_finish: nil, on_progress: nil,
227
233
  on_experiment_start: nil, experimental_selective_replay: nil)
228
234
  Replay.run(
@@ -245,6 +251,7 @@ module Bitfab
245
251
  code_change:,
246
252
  code_change_description:,
247
253
  code_change_files:,
254
+ code_change_base:,
248
255
  experiment_group_id:,
249
256
  experiment_group_name:,
250
257
  dataset_id:,
@@ -149,7 +149,10 @@ RENAMED_FLAGS = {
149
149
  SEED_FLAGS = ("--seed", "--cases", "--from-trace", "--run")
150
150
  PATH_FLAGS = ("--registry", "--params", "--code-change-file")
151
151
  CODE_CHANGE_FLAG = "--code-change"
152
- BRANCH_CODE_CHANGES = ("last-branch", "default-branch")
152
+ CODE_CHANGE_BASE_FLAG = "--code-change-base"
153
+ BRANCH_CODE_CHANGES = ("last-branch", "default-branch", "ref")
154
+ GIT_CODE_CHANGES = ("working-tree", *BRANCH_CODE_CHANGES)
155
+ DESCRIBED_REF = re.compile(r"[^\x00-\x1f\x7f]{1,200}")
153
156
  CHECKOUT_DEPTH = 2
154
157
  CODE_CHANGE_PATH_ENV = "BITFAB_CODE_CHANGE_PATH"
155
158
  CODE_CHANGE_MAX_FILES = 60
@@ -220,7 +223,9 @@ Options added by --cloud:
220
223
  checkout of the repository, with GitHub access that can
221
224
  read it and run Actions (a job token needs contents: read
222
225
  and actions: write, passed as GH_TOKEN), and a workflow on
223
- BRANCH that declares a request input.
226
+ BRANCH that declares a request input. With no snapshot
227
+ there is nothing to diff against, so it takes
228
+ --code-change skip or manual, as does a dispatched run.
224
229
 
225
230
  A workflow can also dispatch bitfab-replay.yml itself on any branch other than a
226
231
  bitfab-replay/ snapshot branch, passing the replay as JSON in the request input:
@@ -1029,8 +1034,30 @@ def default_branch_point(root):
1029
1034
  return {"sha": sha, "ref": default} if sha else None
1030
1035
 
1031
1036
 
1037
+ def chosen_base_point(root, chosen):
1038
+ if not chosen or chosen.startswith("-"):
1039
+ return None
1040
+ for ref in (chosen, f"origin/{chosen}"):
1041
+ if quiet_git(root, "rev-parse", "--verify", "-q", f"{ref}^{{commit}}"):
1042
+ sha = quiet_git(root, "merge-base", "HEAD", ref)
1043
+ return {"sha": sha, "ref": chosen} if sha else None
1044
+ raise ValueError(
1045
+ f"{CODE_CHANGE_BASE_FLAG} {chosen} names no branch or commit in this repository; pass a branch name or a commit SHA, and fetch it first if it only exists on the remote"
1046
+ )
1047
+
1048
+
1049
+ def refuse_git_code_change(replay, where):
1050
+ kind = flag_value(replay, CODE_CHANGE_FLAG)
1051
+ if kind in GIT_CODE_CHANGES:
1052
+ raise ValueError(
1053
+ f"{where} as it is on GitHub, with no snapshot of a checkout, so --code-change {kind} has nothing to compare against there; pass --code-change skip, or --code-change manual with --code-change-file, or start it with bitfab-replay --cloud and no --cloud-ref, which resolves {kind} from your checkout before upload"
1054
+ )
1055
+
1056
+
1032
1057
  def code_change_base(root, replay):
1033
1058
  kind = flag_value(replay, CODE_CHANGE_FLAG)
1059
+ if kind == "ref":
1060
+ return chosen_base_point(root, flag_value(replay, CODE_CHANGE_BASE_FLAG))
1034
1061
  if kind == "last-branch":
1035
1062
  return branch_point(root)
1036
1063
  if kind == "default-branch":
@@ -1839,6 +1866,7 @@ def submit_as_is(root, repo, workflow, parsed, path):
1839
1866
  branch = parsed["cloud"]["--cloud-ref"]
1840
1867
  request, _ = replay_request(root, parsed)
1841
1868
  check_arguments(parsed["replay"])
1869
+ refuse_git_code_change(parsed["replay"], f"--cloud-ref replays {branch}")
1842
1870
  sha = branch_head(repo, branch)
1843
1871
  if "--cloud-preview" in parsed["switches"]:
1844
1872
  return {
@@ -2089,6 +2117,7 @@ def dispatched_request():
2089
2117
  raise ValueError("The request id does not match the execution_id input")
2090
2118
  args = request.get("args")
2091
2119
  validate_arguments(args)
2120
+ refuse_git_code_change(args, f"A dispatched replay runs {branch}")
2092
2121
  if request.get("check") is True and "--dry-run" not in args:
2093
2122
  args = [*args, "--dry-run"]
2094
2123
  normalized = {
@@ -2227,7 +2256,7 @@ def branch_code_change(root, base, commit, request):
2227
2256
  isinstance(sha, str)
2228
2257
  and SHA.fullmatch(sha)
2229
2258
  and isinstance(ref, str)
2230
- and BRANCH_NAME.fullmatch(ref)
2259
+ and DESCRIBED_REF.fullmatch(ref)
2231
2260
  and (
2232
2261
  sha == base
2233
2262
  or quiet_git(root, "rev-parse", "--verify", "-q", f"{commit}^2") == sha
@@ -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
@@ -143,11 +143,11 @@ module Bitfab
143
143
  # Replay historical traces through a traced method and create an experiment.
144
144
  module Replay
145
145
  # The values Client#replay accepts for +code_change:+.
146
- CODE_CHANGE_TYPES = %w[working-tree last-branch default-branch skip manual].freeze
146
+ CODE_CHANGE_TYPES = %w[working-tree last-branch default-branch ref skip manual].freeze
147
147
  # The +code_change:+ choices, as named in the errors that refuse a replay.
148
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)'
149
+ 'branch this one was cut from), "default-branch" (the diff against the default branch), "ref" (the diff ' \
150
+ 'against a branch or commit you name), "skip" (no code change), or "manual" (pass the files yourself)'
151
151
  UNSET_DEFAULT = Object.new.freeze
152
152
  OPTION_DEFAULTS = {mock: "marked"}.freeze
153
153
  PERSISTENCE_TIMEOUT_SECONDS = 30.0
@@ -237,16 +237,22 @@ module Bitfab
237
237
  # diff against the branch this one was cut from, such as the branch below
238
238
  # it in a stack, else the default branch; "default-branch" the diff
239
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.
240
+ # origin/main, origin/master, main, master); "ref" the diff against where
241
+ # HEAD split from the branch or commit in code_change_base; "skip" no code
242
+ # change, such as for a baseline run; "manual" the files in
243
+ # code_change_files. The four git choices include uncommitted edits and
244
+ # untracked files, and warn and attach nothing when they find no changed
245
+ # files.
244
246
  # @param code_change_description [String, nil] description of the code
245
247
  # change, stored on the experiment. Defaults to one generated from the
246
248
  # captured files. Not accepted with "skip".
247
249
  # @param code_change_files [Array<Hash>, nil] the edited files when
248
250
  # code_change is "manual", and only then, each as { path:, before:, after: }
249
251
  # (empty string for new/deleted files).
252
+ # @param code_change_base [String, nil] the branch or commit to diff
253
+ # against when code_change is "ref", and only then, such as "dev",
254
+ # "release/2.3", or a commit SHA. A branch that exists only on the remote
255
+ # is read as origin/<name>. A name git cannot resolve raises ArgumentError.
250
256
  # @param experiment_group_id [String, nil] optional UUID grouping multiple
251
257
  # replay runs into a single experiment batch. The run joins that group,
252
258
  # and an id no group has yet creates it
@@ -313,11 +319,11 @@ module Bitfab
313
319
  # The experiment fields are nil for a dry run, which creates no experiment,
314
320
  # and progress events then carry a nil :experiment_id too.
315
321
  def run(client, receiver, method_name, trace_function_key:, limit: nil, trace_ids: nil, name: nil, notes: nil, metadata: 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,
322
+ concurrency: nil, max_concurrency: ReplayConcurrency::UNSET, code_change: nil, code_change_description: nil, code_change_files: nil, code_change_base: nil, experiment_group_id: nil, experiment_group_name: nil,
317
323
  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,
318
324
  adapt_inputs: nil, mock_override: nil, db_branch: nil, on_item_start: nil, on_item_finish: nil, on_progress: nil,
319
325
  on_experiment_start: nil, experimental_selective_replay: nil)
320
- validate_code_change(code_change, code_change_description, code_change_files)
326
+ validate_code_change(code_change, code_change_description, code_change_files, code_change_base)
321
327
  explicit_options = method(:run).parameters.filter_map { |type, option| [option, binding.local_variable_get(option)] if type == :key }
322
328
  skip_assertion_judging = resolve_skip_assertion_judging(skip_assertion_judging, judge_assertions)
323
329
  concurrency_options = ReplayConcurrency.resolve(concurrency, attempts, max_concurrency)
@@ -396,7 +402,7 @@ module Bitfab
396
402
 
397
403
  include_db_branch_lease = db_branch_enabled?(db_branch) && !dry_run
398
404
 
399
- code_change_description, code_change_files = resolve_code_change(code_change, code_change_description, code_change_files, name)
405
+ code_change_description, code_change_files = resolve_code_change(code_change, code_change_description, code_change_files, name, code_change_base)
400
406
 
401
407
  resolved_db_branch_settings = db_branch_settings(db_branch)
402
408
  replay_data = http_client.start_replay(
@@ -406,6 +412,7 @@ module Bitfab
406
412
  name:,
407
413
  notes:,
408
414
  metadata:,
415
+ code_change_type: code_change,
409
416
  code_change_description:,
410
417
  code_change_files:,
411
418
  experiment_group_id:,
@@ -654,7 +661,7 @@ module Bitfab
654
661
  settings.empty? ? nil : settings
655
662
  end
656
663
 
657
- def validate_code_change(code_change, description, files)
664
+ def validate_code_change(code_change, description, files, base = nil)
658
665
  if code_change.nil?
659
666
  raise ArgumentError, "replay needs a code_change naming the code this run tests: #{CODE_CHANGE_CHOICES}."
660
667
  end
@@ -668,17 +675,24 @@ module Bitfab
668
675
  if code_change != "manual" && !files.nil?
669
676
  raise ArgumentError, "code_change_files only applies with code_change \"manual\" (got \"#{code_change}\")."
670
677
  end
678
+ if code_change == "ref" && !(base.is_a?(String) && !base.strip.empty? && !base.strip.start_with?("-"))
679
+ raise ArgumentError,
680
+ 'code_change "ref" needs code_change_base: the branch or commit to diff against, such as "dev" or a commit SHA.'
681
+ end
682
+ if code_change != "ref" && !base.nil?
683
+ raise ArgumentError, "code_change_base only applies with code_change \"ref\" (got \"#{code_change}\")."
684
+ end
671
685
  if code_change == "skip" && !description.nil?
672
686
  raise ArgumentError, 'code_change "skip" attaches no code change, so it cannot take a code_change_description.'
673
687
  end
674
688
  code_change
675
689
  end
676
690
 
677
- def resolve_code_change(code_change, description, files, label = nil)
691
+ def resolve_code_change(code_change, description, files, label = nil, base = nil)
678
692
  return [nil, nil] if code_change == "skip"
679
693
  return [description, files] if code_change == "manual"
680
694
 
681
- captured = read_code_change_file || capture_code_change_from_git(Dir.pwd, code_change, label)
695
+ captured = read_code_change_file || capture_code_change_from_git(Dir.pwd, code_change, label, base&.strip)
682
696
  if captured.nil? || captured[1].nil? || captured[1].empty?
683
697
  warn "Bitfab: code_change \"#{code_change}\" found no changed files, so this replay has no code change attached."
684
698
  return [description, nil]
@@ -712,8 +726,21 @@ module Bitfab
712
726
  !git(root, ["rev-parse", "--verify", "#{ref}^{object}"]).nil?
713
727
  end
714
728
 
715
- def cc_resolve_base(root, type)
729
+ def cc_resolve_chosen_base(root, chosen)
730
+ [chosen, "origin/#{chosen}"].each do |ref|
731
+ next unless cc_ref_exists?(root, "#{ref}^{commit}")
732
+
733
+ base = (git(root, ["merge-base", "HEAD", ref]) || "").strip
734
+ return base.empty? ? nil : [base, "vs #{chosen}"]
735
+ end
736
+ raise ArgumentError,
737
+ "code_change_base \"#{chosen}\" names no branch or commit in this repository. Pass a branch name, such as " \
738
+ "\"dev\", or a commit SHA; fetch it first if it only exists on the remote."
739
+ end
740
+
741
+ def cc_resolve_base(root, type, chosen = nil)
716
742
  case type
743
+ when "ref" then chosen ? cc_resolve_chosen_base(root, chosen) : nil
717
744
  when "last-branch" then cc_resolve_branch_point(root)
718
745
  when "default-branch" then cc_resolve_default_branch_point(root)
719
746
  else cc_ref_exists?(root, "HEAD") ? ["HEAD", "uncommitted (vs HEAD)"] : nil
@@ -808,12 +835,12 @@ module Bitfab
808
835
  0
809
836
  end
810
837
 
811
- def capture_code_change_from_git(cwd, type, label = nil)
838
+ def capture_code_change_from_git(cwd, type, label = nil, chosen = nil)
812
839
  root = git(cwd, ["rev-parse", "--show-toplevel"])
813
840
  return nil if root.nil? || root.strip.empty?
814
841
 
815
842
  root = root.strip
816
- resolved = cc_resolve_base(root, type)
843
+ resolved = cc_resolve_base(root, type, chosen)
817
844
  return nil if resolved.nil?
818
845
 
819
846
  entries = cc_list_changes(root, resolved[0])
@@ -848,6 +875,8 @@ module Bitfab
848
875
  head = ((label && !label.strip.empty?) ? label.strip : nil) || (subject.empty? ? nil : subject) || "Working-tree change"
849
876
  word = (files.length == 1) ? "file" : "files"
850
877
  ["#{head} (#{files.length} #{word} changed #{against})", files]
878
+ rescue ArgumentError
879
+ raise
851
880
  rescue
852
881
  nil
853
882
  end
@@ -204,6 +204,7 @@ module Bitfab
204
204
  end
205
205
 
206
206
  options[:code_change] = args.fetch(:code_change)
207
+ options[:code_change_base] = args[:code_change_base] if args[:code_change_base]
207
208
  if args[:code_change_file]
208
209
  code_change = load_code_change(args[:code_change_file])
209
210
  options[:code_change_description] = code_change.fetch("description")
@@ -337,10 +338,11 @@ module Bitfab
337
338
  options.on("--max-concurrency N", Integer) do |value|
338
339
  args[:max_concurrency] = positive_integer("--max-concurrency", value)
339
340
  end
340
- options.on("--code-change working-tree|last-branch|default-branch|skip|manual",
341
+ options.on("--code-change working-tree|last-branch|default-branch|ref|skip|manual",
341
342
  "Required: the code change this run tests; working-tree is the uncommitted changes vs HEAD, " \
342
343
  "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
+ "default branch, ref the diff against --code-change-base, skip none, manual the edits in " \
345
+ "--code-change-file.") do |value|
344
346
  unless Replay::CODE_CHANGE_TYPES.include?(value)
345
347
  raise ArgumentError,
346
348
  "Unknown --code-change '#{value}'. Pass #{Replay::CODE_CHANGE_CHOICES}. " \
@@ -350,6 +352,8 @@ module Bitfab
350
352
  end
351
353
  options.on("--code-change-file PATH",
352
354
  "With --code-change manual: a JSON file of { description, files }.") { |value| args[:code_change_file] = value }
355
+ options.on("--code-change-base BRANCH|COMMIT",
356
+ "With --code-change ref: the branch or commit to diff against.") { |value| args[:code_change_base] = value }
353
357
  options.on("--experiment-group-id UUID",
354
358
  "Add this run to an experiment group. A UUID no group has yet creates the group.") { |value| args[:experiment_group_id] = value }
355
359
  options.on("--experiment-group-name NAME",
@@ -390,12 +394,17 @@ module Bitfab
390
394
  def require_code_change(args)
391
395
  if args[:code_change].nil?
392
396
  raise ArgumentError,
393
- "--code-change is required: #{Replay::CODE_CHANGE_CHOICES}. \"manual\" takes the edits from --code-change-file PATH."
397
+ "--code-change is required: #{Replay::CODE_CHANGE_CHOICES}. \"ref\" takes the branch or commit from " \
398
+ "--code-change-base, and \"manual\" takes the edits from --code-change-file PATH."
394
399
  end
395
- return args if (args[:code_change] == "manual") == !args[:code_change_file].nil?
400
+ unless (args[:code_change] == "manual") == !args[:code_change_file].nil?
401
+ raise ArgumentError,
402
+ "--code-change-file goes with --code-change manual, and --code-change manual needs --code-change-file PATH."
403
+ end
404
+ return args if (args[:code_change] == "ref") == !args[:code_change_base].nil?
396
405
 
397
406
  raise ArgumentError,
398
- "--code-change-file goes with --code-change manual, and --code-change manual needs --code-change-file PATH."
407
+ "--code-change-base goes with --code-change ref, and --code-change ref needs --code-change-base BRANCH|COMMIT."
399
408
  end
400
409
 
401
410
  def registry_dataset_ids(options)
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Bitfab
4
- VERSION = "0.69.0"
4
+ VERSION = "0.71.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.69.0
4
+ version: 0.71.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Harvest Team