fix(review): stop stubbing files whose worktree content the size probe cannot find - #1220
Merged
Conversation
…e cannot find The oversized preflight in buildBoundedTrackedDiff mapped every object the cat-file batch could not size to infinity. `missing` is routine: for tree-vs-worktree diffs git hashes the WORKING-TREE content of any path pulled into rename/copy detection and prints that hash in --raw output without ever writing the blob, and partial clones report it for unfetched blobs. Those files were excluded by pathspec and replaced with a contents-free binary stub, so a renamed-and-edited file rendered as a silently empty card and /api/file-content refused it as binary. Missing now means unknown, not oversized, and an unreadable new side is bounded by the working-tree file's stat size instead. Every rendered diff stays bounded git-side by core.bigFileThreshold (#1205), genuinely oversized files still stub via real probe sizes plus the stat door, and a blob git truly cannot read now fails loudly through assertGitSuccess instead of blanking a file. Client side, a chunk with a binary marker and no hunks now renders an explicit placeholder in both the all-files view and the single-file viewer, so an empty card can never again pass for "no changes here". AI-assisted.
Harmonizes with #1219's oversized-file notice at the three points they overlap. diff-paths.ts keeps both detectors, ordered specific then general: isOversizedReviewStubPatch (marker-based, from #1219) stays first, and isContentlessBinaryPatch follows as the general fallback with a doc note that callers ask the marker first. Both mount points render exactly one notice. A marker-carrying stub gets OversizedFileNotice, which knows the file is over the size cap; anything else hunkless and binary (a genuine binary, or a stub shape the marker does not cover) gets BinaryFileNotice. The fallback is gated on the marker so the two can never stack on one card. The new DOM test moves into #1219's isolated real-renderer CI step rather than the shared list, for the same reason that step exists.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
TLDR
In v0.26.0 and v0.26.1, code review silently drops file content from diffs. A file that was renamed and then edited, and any file in a partial clone whose blob is not local, renders as an empty card: no hunks, no counts, no explanation.
/api/file-contentthen refuses it as binary, so there is no way to recover the content in the UI. A reviewer can approve a change without ever seeing it.On the harness fixture the renamed-and-edited
Panel.tsx(a real+8/-2) shows ash0 +0/-0on both released versions. Nothing in the product says anything is missing.Mechanism
buildBoundedTrackedDiff(packages/shared/review-core.ts) runs an oversized preflight:git diff --no-textconv --raw -z --no-abbrev <base>, then batch-probes every object id withcat-file --batch-check.For tree-vs-worktree diffs, any path pulled into rename/copy detection has its working-tree content hashed by git, and that computed hash is printed in
--rawoutput. The blob is never written to the object database.getGitObjectSizesskippedmissinganswers, and the backfill loop mapped every id it had no size for toNumber.POSITIVE_INFINITY.The consumer only opened the filesystem-stat door for all-zero object ids (
isNullObjectId), so those entries were read as oversized, excluded with a:(top,exclude,literal)pathspec, and replaced bybuildOversizedTrackedStub: zero hunks plusBinary files ... differ. The client parsed that to 0/0 and rendered a header with an empty body, and theisBinaryPatchFilegate in/api/file-content(both the Bun server and the Pi mirror) then returned nulls.Verified affected shapes:
Affected views: since-base, uncommitted, unstaged, and the GitButler and jj callers of
getWorkingTreeDiffFromBase. Affected versions: v0.26.0, v0.26.1, and dev.The fix
1. The stat door now covers unreadable new sides, not just unhashed ones.
workingTreeInfois resolved whenever the new side has no readable object behind it: an all-zero id, or an id the probe that ran could not find. The working-tree file is exactly what git formats for those entries, so its stat size is the authoritative bound. This is the invariant already documented above the loop:core.bigFileThresholddoes not bound working-tree sides, so their sizes have to come from stat.2. Missing means unknown-but-bounded, never infinite.
getGitObjectSizesnow returnsMap<string, number | null>, wherenullis "the probe ran and could not size this object" (missing, omitted, or an unparseable size). An unknown blob size reads as not-oversized, which is safe because every rendered diff already carriesBOUNDED_DIFF_GIT_CONFIG(core.bigFileThreshold, from #1205), so git itself stubs oversized object-database content. A missing blob must never stub a file git can plainly diff. If git genuinely cannot produce the content (a dead promisor), the diff now fails loudly throughassertGitSuccessrather than silently blanking a file. Genuinely oversized files still stub, via real probe sizes and the stat door.The door being closed is the per-object missing door only. #1205's invariants are preserved exactly:
core.bigFileThresholdrather than on a working-tree file that may not even be the diff's new side (a staged or commit-to-commit diff's new side is not the worktree).3. An empty card now says why it is empty. New shared predicate
isContentlessBinaryPatchinpackages/shared/diff-paths.ts(binary marker, no hunks) plus a smallBinaryFileNoticecomponent, mounted in both the all-files view (AllFilesCodeView) and the single-file viewer (DiffViewer). Copy: "Binary or oversized file, content not shown." This is defense in depth: whatever the cause, a card with no body now states that content was not shown instead of reading as "no changes here".Failing-then-passing proof
New tests drive the real
runGitDiff/getWorkingTreeDiffFromBaseagainst scratch git repos built to the reproduction shapes.similarity index 94% / rename ... / Binary files a/src/Card.tsx and b/src/Panel.tsx differnew file mode 100644 / Binary files /dev/null and b/replacement.txt differnew file mode 100644 / Binary files /dev/null and b/added.txt differThe #1205 test
a single object the probe reports missing still excludes only that pathasserted the exact door being closed, so it is rewritten rather than deleted, and it now covers strictly more: a probed-oversized object still excludes only its own path (asserted on the emitted pathspec list), while a missing one excludes nothing and keeps rendering. Plus six unit tests forisContentlessBinaryPatch, including a text patch whose content quotes the binary marker.Full runs:
bun test2936 pass / 0 fail;bun test apps/pi-extension177 pass / 0 fail (Pi vendorsreview-core.tsanddiff-paths.tsverbatim throughvendor.sh, so parity is automatic);bun run typecheckclean;bun run --cwd apps/review buildthenbun run build:hookboth clean.Version-matrix harness (dev, same fixture, since-base)
Panel.tsx(renamed + edited)/api/file-contenton Panel.tsxdev now matches git ground truth on every file, with one deliberate difference: the 6 MB file stays a binary stub, which is #1205's memory bound doing its job. That single stubbed line is the whole +34 vs +35 gap. The renamed file is back to real content and its file-content probe is non-null again.
Lineage
#1167 is the size-cap work this preflight came from; #1205 made the memory bound git-native (
core.bigFileThreshold) so a failed probe stops blanking the whole review. This change closes the remaining per-object door, themissingone, and keeps the memory bound exactly where #1205 put it.Merged with #1219
#1219 landed first, so main is merged in here and the predicted overlap is resolved in this branch. Rule applied: specific beats general, and the two never stack.
packages/shared/diff-paths.tskeeps both detectors, ordered specific then general.isOversizedReviewStubPatch(marker-based, from fix(review): mint content-derived diff cache keys so single-file tabs render fully #1219) stays first;isContentlessBinaryPatchfollows as the documented fallback, with a note that callers ask the marker first. No duplicate helpers.AllFilesCodeViewandDiffViewer) render exactly one notice: a marker-carrying stub gets fix(review): mint content-derived diff cache keys so single-file tabs render fully #1219'sOversizedFileNotice, which knows the file is over the size cap; anything else hunkless and binary (a genuine binary, or a stub shape the marker does not cover) getsBinaryFileNotice. The fallback is explicitly gated on the marker so the two can never both appear on one card.review-core.tsmerged without conflict: fix(review): mint content-derived diff cache keys so single-file tabs render fully #1219's marker injection is inbuildOversizedTrackedStub, this change is ingetGitObjectSizesand the preflight loop.afterAllrestore of the leakedmock.module('@pierre/diffs')inAllFilesCodeView.lifecycle.test.tsxis untouched.A new DOM case pins the harmonization directly: a marker-carrying stub renders exactly one oversized notice and zero fallback notices.
Post-merge verification:
bun test2938 pass / 0 fail; all three DOM CI steps green, with the shared list and the isolated diff-renderer step each run forward and reversed (226 pass, 8 pass, 6 pass); typecheck clean; review and hook builds clean. The harness re-run on dev still reports +34/-22,Panel.tsxback toR h2 +8/-2with real content,bigassets/metrics-6mb.txtthe only remaining stub, and its served patch carries #1219's marker, so it renders the specific size-cap notice rather than the fallback.AI-assisted.