bitfab 0.30.0 → 0.30.1

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: 1711bd0806a246ec36f15e2bedd0b31d8fe8c9df0aacc922d7ad15a20badf629
4
- data.tar.gz: 2e619cf976e2909bdf1bf629cb350e6b761115d4ec9d8372beaacfe5b46adbbf
3
+ metadata.gz: 4a3e03384934c51dd57e27ac1213dca8f88b41ead5a0980c04bb10ba0ad464d8
4
+ data.tar.gz: da9c5574b00bd66ba481a78e0f1e0ce4f8e9336b35a7b2e9013498b475622237
5
5
  SHA512:
6
- metadata.gz: 88d118025460c8aed53e48d4ca16815133c67aa04986688bf1ed84ed506a16c41f11e76fd012cb9c18f6ee06a44f1b42e7de811e78834da737b9e4b260d5a024
7
- data.tar.gz: 6c0e746e23dfdf5995c502e3a40cc12c03888c5e783b924049d2cd4152660c8a226b9ce852532440687630449fe84f027a75082602f175619fbe28e38121e9ba
6
+ metadata.gz: 97f2755495103e92c94258f1763a1066bfc5a1944168e10dec1ad1e0bd24a58ca916791122cf3673f52326ca0d5b89b7ca8324071d40b6c408f3153167f6c95c
7
+ data.tar.gz: 61ba2d4b92cfa2c2665753319c6acf67dda2d48a547088d1ab47b2e9006835fe869185f3861d4e378f5c8e76167009f2580f385b5f6416467a24ae5b9dce8242
data/lib/bitfab/replay.rb CHANGED
@@ -2,6 +2,8 @@
2
2
 
3
3
  require "fileutils"
4
4
  require "json"
5
+ require "open3"
6
+ require "timeout"
5
7
 
6
8
  require_relative "constants"
7
9
  require_relative "mock_override"
@@ -179,6 +181,19 @@ module Bitfab
179
181
 
180
182
  include_db_branch_lease = !environment.nil?
181
183
 
184
+ # An explicit code change always wins; only when the caller passed neither
185
+ # field do we fall back to the payload the replay wrapper captured from git
186
+ # and handed over via BITFAB_CODE_CHANGE_PATH. This is what makes an
187
+ # experiment show its diff even when the run happened outside the
188
+ # assistant's make-change rails.
189
+ if code_change_description.nil? && code_change_files.nil?
190
+ captured = resolve_auto_code_change(name)
191
+ unless captured.nil?
192
+ code_change_description = captured[0]
193
+ code_change_files = captured[1]
194
+ end
195
+ end
196
+
182
197
  replay_data = http_client.start_replay(
183
198
  trace_function_key,
184
199
  effective_limit,
@@ -295,6 +310,177 @@ module Bitfab
295
310
  result
296
311
  end
297
312
 
313
+ # Read the code-change payload the replay wrapper captured and pointed us at
314
+ # via BITFAB_CODE_CHANGE_PATH. Returns [description, files] or nil.
315
+ # Best-effort: any error (no env var, missing file, bad JSON) yields nil so
316
+ # the replay proceeds with no code change rather than raising.
317
+ TRUNK_CANDIDATES = ["origin/HEAD", "origin/main", "origin/master", "main", "master"].freeze
318
+ CC_MAX_FILES = 60
319
+ CC_MAX_FILE_BYTES = 500_000
320
+ CC_MAX_TOTAL_BYTES = 2_000_000
321
+
322
+ # Auto-capture the code change to attach when the caller passed none. Lives
323
+ # in replay() (the one call guaranteed to run on every replay) so it works no
324
+ # matter how the replay was launched. Precedence: a BITFAB_CODE_CHANGE_PATH
325
+ # override, else the git diff vs trunk. Best-effort: any failure yields nil.
326
+ # An explicit code_change_files always wins over this.
327
+ def resolve_auto_code_change(label = nil)
328
+ return nil if ENV["BITFAB_DISABLE_CODE_CHANGE_CAPTURE"]
329
+
330
+ from_env = read_code_change_file
331
+ return from_env unless from_env.nil?
332
+
333
+ capture_code_change_from_git(Dir.pwd, label)
334
+ end
335
+
336
+ def read_code_change_file
337
+ path = ENV["BITFAB_CODE_CHANGE_PATH"]
338
+ return nil if path.nil? || path.empty?
339
+
340
+ begin
341
+ parsed = JSON.parse(File.read(path))
342
+ files = parsed["files"]
343
+ # A malformed payload (non-array, or entries that aren't hashes) must
344
+ # yield no code change, never crash normalize_code_change_files.
345
+ files = nil unless files.is_a?(Array) && files.all?(Hash)
346
+ description = parsed["description"]
347
+ description = nil unless description.is_a?(String)
348
+ return nil if files.nil? && description.nil?
349
+
350
+ [description, files]
351
+ rescue
352
+ nil
353
+ end
354
+ end
355
+
356
+ def git(cwd, args)
357
+ # capture3 so git's stderr (e.g. "not a git repository") is swallowed, not
358
+ # leaked to the host process's console.
359
+ out, _err, status = Timeout.timeout(30) do
360
+ Open3.capture3("git", *args, chdir: cwd)
361
+ end
362
+ return nil unless status.success?
363
+
364
+ # Tag as UTF-8 and scrub invalid bytes so blob contents compare cleanly
365
+ # against the UTF-8 working-file reads and never break JSON.generate later
366
+ # (a non-UTF-8 file that slips past the null-byte check would otherwise
367
+ # abort the whole start-replay request).
368
+ out.force_encoding("UTF-8").scrub
369
+ rescue
370
+ nil
371
+ end
372
+
373
+ def cc_ref_exists?(root, ref)
374
+ !git(root, ["rev-parse", "--verify", "#{ref}^{object}"]).nil?
375
+ end
376
+
377
+ # Returns [base, from_trunk]; from_trunk is false on the HEAD fallback.
378
+ def cc_resolve_base(root)
379
+ forced = ENV["BITFAB_CODE_CHANGE_BASE"]
380
+ if forced && cc_ref_exists?(root, forced)
381
+ mb = git(root, ["merge-base", "HEAD", forced])
382
+ return [mb.strip, true] if mb && !mb.strip.empty?
383
+
384
+ rp = git(root, ["rev-parse", "--verify", forced])
385
+ return (rp && !rp.strip.empty?) ? [rp.strip, true] : nil
386
+ end
387
+ TRUNK_CANDIDATES.each do |candidate|
388
+ next unless cc_ref_exists?(root, candidate)
389
+
390
+ mb = git(root, ["merge-base", "HEAD", candidate])
391
+ return [mb.strip, true] if mb && !mb.strip.empty?
392
+ end
393
+ cc_ref_exists?(root, "HEAD") ? ["HEAD", false] : nil
394
+ end
395
+
396
+ def cc_blob_bytes(root, base, path)
397
+ out = git(root, ["cat-file", "-s", "#{base}:#{path}"])
398
+ (out && !out.strip.empty?) ? out.strip.to_i : 0
399
+ rescue
400
+ 0
401
+ end
402
+
403
+ def cc_working_bytes(root, path)
404
+ File.size(File.join(root, path))
405
+ rescue
406
+ 0
407
+ end
408
+
409
+ def capture_code_change_from_git(cwd, label = nil)
410
+ root = git(cwd, ["rev-parse", "--show-toplevel"])
411
+ return nil if root.nil? || root.strip.empty?
412
+
413
+ root = root.strip
414
+ resolved = cc_resolve_base(root)
415
+ return nil if resolved.nil?
416
+
417
+ base, from_trunk = resolved
418
+
419
+ tracked = git(root, ["diff", "--name-status", "--no-renames", "-z", base, "--", ":!.bitfab"]) || ""
420
+ untracked = git(root, ["ls-files", "--others", "--exclude-standard", "-z", "--", ":!.bitfab"]) || ""
421
+
422
+ entries = cc_parse_name_status_z(tracked)
423
+ untracked.split("\0").reject(&:empty?).each { |p| entries << ["A", p] }
424
+ return nil if entries.empty?
425
+
426
+ files = []
427
+ total = 0
428
+ entries.each do |status, path|
429
+ break if files.length >= CC_MAX_FILES
430
+
431
+ # Skip oversized files by size BEFORE reading their full contents, so a
432
+ # huge changed file can't OOM/stall replay just to be discarded.
433
+ before_bytes = (status == "A") ? 0 : cc_blob_bytes(root, base, path)
434
+ after_bytes = (status == "D") ? 0 : cc_working_bytes(root, path)
435
+ next if before_bytes > CC_MAX_FILE_BYTES || after_bytes > CC_MAX_FILE_BYTES
436
+
437
+ # Normalize CRLF -> LF on both sides: git's blob is already LF-normalized
438
+ # (autocrlf clean), so a raw CRLF working file would otherwise skew every
439
+ # line as changed. Comparing/storing LF keeps the diff meaningful.
440
+ before = (status == "A") ? "" : (git(root, ["show", "#{base}:#{path}"]) || "").gsub("\r\n", "\n")
441
+ after = (status == "D") ? "" : cc_read_working_file(root, path).gsub("\r\n", "\n")
442
+ next if before == after
443
+
444
+ size = before.bytesize + after.bytesize
445
+ next if total + size > CC_MAX_TOTAL_BYTES ||
446
+ cc_looks_binary?(before) || cc_looks_binary?(after)
447
+
448
+ total += size
449
+ files << {"path" => path, "before" => before, "after" => after}
450
+ end
451
+ return nil if files.empty?
452
+
453
+ subject = git(root, ["log", "-1", "--format=%s", "HEAD"])
454
+ subject = subject ? subject.strip : ""
455
+ head = ((label && !label.strip.empty?) ? label.strip : nil) || (subject.empty? ? nil : subject) || "Working-tree change"
456
+ word = (files.length == 1) ? "file" : "files"
457
+ against = from_trunk ? "vs trunk" : "uncommitted (vs HEAD)"
458
+ ["#{head} (#{files.length} #{word} changed #{against})", files]
459
+ rescue
460
+ nil
461
+ end
462
+
463
+ def cc_parse_name_status_z(raw)
464
+ parts = raw.split("\0").reject(&:empty?)
465
+ out = []
466
+ i = 0
467
+ while i + 1 < parts.length
468
+ out << [parts[i][0], parts[i + 1]]
469
+ i += 2
470
+ end
471
+ out
472
+ end
473
+
474
+ def cc_read_working_file(root, path)
475
+ File.read(File.join(root, path), encoding: "UTF-8")
476
+ rescue
477
+ ""
478
+ end
479
+
480
+ def cc_looks_binary?(str)
481
+ str[0, 8000].include?("\0")
482
+ end
483
+
298
484
  def write_replay_result_file(result)
299
485
  result_path = ENV["BITFAB_REPLAY_RESULT_PATH"]
300
486
  return if result_path.nil? || result_path.empty?
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Bitfab
4
- VERSION = "0.30.0"
4
+ VERSION = "0.30.1"
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.30.0
4
+ version: 0.30.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Harvest Team