[Move] Patch apply and revert onto the bundled Git (#385) - #410
Conversation
|
@coderabbitai review |
|
Warning Review limit reachedNext included review available in 29 minutes. View limit detailsLimit details: You’ve used the included review currently available. You've used all free OSS reviews for now. Wait for the free limit to reset to keep reviewing this public repository. Review configuration: ⚙️ Run configurationConfiguration used: Path: .coderabbit.yaml Review profile: CHILL Plan: Advanced Run ID: ⛔ Files ignored due to path filters (1)
📒 Files selected for processing (19)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
|
7aab622 to
521f0c7
Compare
|
Windows walkthrough, 2026-09-09, Windows 11 VM with no Git installed, Buildkite artifact of #411 ( With ticket 60001 linked and an edit in the WIP: apply PR #13444 of wordpress-develop (2 files under |
521f0c7 to
2ee5620
Compare
2ee5620 to
84e76e6
Compare
84e76e6 to
98c4c9c
Compare
98c4c9c to
aab02ad
Compare
aab02ad to
2094034
Compare
2094034 to
ecb4787
Compare
git apply decides and writes now: whether a patch fits is the question the mentor who receives it, or Trac's committer, asks the same tool. It is all or nothing on its own, it refuses a path outside the tree or through a symbolic link, and it applies binary sections that carry their data, renames, mode changes and empty files. Decided 2026-09-08 after probing Git 2.53 for each of those. What stays in JS is what Git does not give: the rewrite of Trac's paths to today's layout, applied to the text so `-p1` lands them where the preview said; a binary section with no data left out and named as skipped; the wording of a refusal, which files and inside each which regions and why, from the per-hunk diagnosis that never writes; the "that patch is not here any more" answer, a reverse that fails everywhere while the forward patch would apply; and a snapshot of the files the patch names, so a write Git left half done (an I/O failure part-way, which Git does not roll back) is put back. The applier keeps its signature and every result shape, so main, the renderer and the IPC tests do not change; the three patch journeys and the revert after a restart are the judge and are untouched. ensureAutocrlf and createCrlfCompatibleFs retire with their tests: an in-memory view built for isomorphic-git, a no-op at every call site since the reads moved. isomorphic-git becomes a development dependency, for the test fixtures only (it stays in the packaged app through @wp-playground/storage). A new test proves every shape the app's own generator emits passes git apply --check. One thing got worse and is documented: a CRLF file in a checkout a host Git made on macOS receives LF lines from Git where the old applier matched its ending; on Windows the core.autocrlf view every worktree command carries covers it, and a site the app cloned is LF throughout. Part of #385. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01J6koA9mvjSExVaKJp4nvSx
A patch whose only sections are data-less binaries succeeds with them named again, as it did before the move. The preview tells a binary that carries its data from one that does not, so what it calls skipped is what the apply skips; on the way, parsePatchFiles parses section by section, because jsdiff swallows a section with no hunks whenever another follows it and a binary or a pure rename ahead of a text file was missing from the preview already. A refusal resolves the worktree view once and checks at most twenty sections one by one, saying how many it did not. The rollback leaves a file whose bytes still match the snapshot alone. The guide no longer claims a pull request's diff carries binary data. Tests for each, and for the two shapes the review named untested: Git's own line as the only reason, and a rename onto a file that exists. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01J6koA9mvjSExVaKJp4nvSx
Thirteen unit tests failed on Windows: every byte-for-byte assertion on what git apply wrote, or what a generated patch put into a second checkout, saw CRLF where the patch said LF. The fixtures had no core.autocrlf in their repository config, which is the shape of a checkout a host Git made, and for that shape the app deliberately carries the Windows CRLF view (crlfArgs, #341) into every worktree command, git apply included. The app was doing what it promises; the fixtures were asking about a site the app cloned while looking like one it adopted. patchRepo in ipc-wiring and makeRepo in the applier suite now write core.autocrlf=false, as git-clone.cjs does. The one test that is about the adopted shape, the LF patch fitting a CRLF file under the Windows view, opts out and keeps its repository unconfigured. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015pwaiJL5hyca8hhrBJ8qaS
Both inject an I/O failure with a regular file where the patch wants a directory. POSIX Git fails with ENOTDIR part-way, which is what the snapshot-and-rollback path is for; the bundled Git on Windows exits 0 on the same patch, so the injection does not inject and the tests read a success where they expect a failure. Whether Git wrote nothing and said so, or --check would have refused first, needs a Windows machine: #413 carries the hypothesis and the questions. Until then the rollback path is covered on POSIX only, and the skip says why. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015pwaiJL5hyca8hhrBJ8qaS
ecb4787 to
4b34b76
Compare
Why
src/patch-apply.jswas the last engine the app carried of its own: 549 lines matching and writing hunks with thediffpackage, plus a path sandbox, a rollback, CRLF preservation, rename handling and per-hunk diagnosis, all written whenisomorphic-githad no merge machinery. With #401 to #408 below it, "does this patch fit this checkout" can be answered by the samegit applya mentor or Trac's committer will run, and the write can be Git's. Decided on 2026-09-08 with JuanMa: move togit apply, hybrid, withtests/e2e/journeys/patch-apply.spec.jsas the judge. Stacked on #408.What changes
src/git-write.cjs:applyPatch(dir, text, { check, reverse, platform }):apply --whitespace=nowarn -p1 [--check] [--reverse] -on stdin,okCodes [0, 1, 128], thewindowsArgsprefix (thecore.autocrlfview is what lets an LF patch fit a CRLF checkout a host Git made on Windows). No--index: the index stays untouched and a new file stays untracked, as before. Stderr is kept for the log and never parsed.src/patch-plan.cjs:rewritePatchPaths(text)puts every header path (diff --git,Index:,---/+++,rename/copy from/to) through the samestripPathPrefix+mapToSrcLayoutthe parser uses and re-emitsa//b/, so-p1lands them where the preview said; quoted paths are refused as they already were.splitPatchSections(text)cuts the text per file (path,from, binary with or without data), so one file can be checked alone and a data-less binary section left out;parsePatchFilesparses section by section over it, so a binary or a pure rename ahead of a text file is no longer swallowed by jsdiff.src/patch-apply.jsrewritten around Git, same signature and result shapes (main.js,apply-conflict.cjsand the IPC tests unchanged): parse, rewrite, drop "Binary files differ" sections intoskipped(a section that carries its data now applies), whole--check; on refusal,--checkper section to name the files, worded in JS (target missing or present, "points outside the site folder" with symlinks followed, elsediagnoseHunksregions, kept verbatim);notApplied(Revert dead-ends when the patch is no longer in the checkout #183) when every section fails to reverse and the whole patch applies forwards; on a clean check, a snapshot of both ends of every section, the write, androllback(dir, snapshot)if Git exits non-zero after all (an I/O failure part-way, which Git does not undo; verified). Gone:resolveFile,resolveInside,dominantEol,patchIsAbsent, the write loop.src/trunk-update.js:ensureAutocrlf/createCrlfCompatibleFsdeleted (an in-memory view built forisomorphic-git, a no-op at every call site since the reads moved) with their seven tests; the three call sites inmain.jsand the applier go;src/pr-files.cjs's comment sayscrlfArgs.package.json:isomorphic-gitis adevDependency(four test fixtures still build repositories with it, until Phase 4: isomorphic-git leaves, and the issues parked on this decision are re-asked #386). It stays in the packaged app all the same:@wp-playground/storagedepends on it, so the installer does not shrink here.docs/guide/applying-patches.md(the apply isgit apply, all or nothing; binaries with data apply, data-less ones are named),docs/guide/getting-started.md(no flow is onisomorphic-git), AGENTS.md and the review standard (patch apply ongit applythrough the primitives; the JS keeps the path rewrite, the wording and the snapshot, none of which writes; the generator's output must passgit apply --check), the self-review skill's list.applyPatchargv and integration (check writes nothing, apply and reverse, index untouched, a path outside refused with 128, CRLF under the Windows view);rewritePatchPathsandsplitPatchSectionson every header shape; the applier's 45 tests kept where they describe behaviour, with the internals' tests replaced (rollback over a snapshot, the CRLF case under both platforms, a binary with data applied); a new agreement test inipc-wiring: every shapegit:get-patchemits (deletion, addition, empty add and delete, no trailing newline, a binary named above) passesgit apply --checkand applies in a second checkout. Journeys unchanged: 18/18.Diff size. ~1,400 changed lines outside the lock in two commits (the move, then the self-review answers), more than half of them tests moved or deleted (the seven CRLF-view tests, the internals' tests,
patch-apply.jsitself losing 300).How to test this
Platforms: macOS and Windows.
Starting state: the Buildkite artifact for the current head installed; a site created by this build with a ticket linked.
wordpress-develop). Expected: the preview listssrc/…paths; Apply and rebuild applies, the banner says what is applied;git status --porcelainin the site folder shows the files unstaged (M,??), nothing staged. Revert this patch: the checkout is back,git status --porcelainclean.wp-admin/…, noa/b/): the preview namessrc/wp-admin/…and the apply lands there..diffmade withgit diff --binarythat adds an image, applied from a file. Expected: the preview does not list it as skipped and the image lands. A.patchthat only says "Binary files differ" (a pull request's diff from GitHub, a Trac attachment): the preview names it as skipped and the apply skips it.git applyis the bundled one (ps/ Task Manager showsdugite/git/bin/git applyduring step 1, briefly).What must not have happened: anything staged by an apply; a half-applied patch after a refusal (
git statusunchanged); a file written outside the site folder from a crafted patch (Git refuses../and symlinked paths:error: invalid path).Risks and limitations
core.autocrlf, a site the app cloned is LF throughout, and Windows is covered by thecore.autocrlfview; documented in the module header.rolledBack: false,recovery).diffpackage's guess (evidence, not proof, as before); Git's decision is the one that writes.--checkwould have refused first, needs a Windows machine; until then the snapshot-and-rollback path is covered on POSIX only. Found on this PR's Windows CI together with a fixture-shape problem (the patch fixtures looked like adopted checkouts, so Windows saw CRLF where the patch said LF), fixed in684e220.Related
Part of #364. Closes #385: this is the last of its four flows, and every box on that issue is ticked by #403, #404, #405, #407, #408 and this PR. Stacked on #408 (merge that first). Next: #411 retires
isomorphic-gitfrom the fixtures andpackage.json. Follow-up: #413.Design decisions and alternatives considered
git applysays "patch failed: file:line" and no more; the per-hunk "already applied" versus "moved" with a line to search for (A patch that no longer applies is a dead end, and the app is already holding the way out #282, "No longer applies" hides a patch that is already in the tree — sometimes in the same file as one that is not #226) is what the panel is built on, sodiagnoseHunksstays as wording over the files Git named. Nothing in JS writes any more.--checkto name failures, not stderr parsing. The standard forbids parsing human-facing output; a section checked on its own answers with an exit code.--directory. The mapping is per file (root files stay,wp-*moves undersrc/), which a single--directoryprefix cannot express.reverseFile. Only to word a failing reverse (Tell the contributor's own edits apart from the patch they applied #306); Git does its own--reverse.isomorphic-gittodevDependenciesnow, not in Phase 4: isomorphic-git leaves, and the issues parked on this decision are re-asked #386: nothing undersrc/requires it, and arequiresneaking back in would fail the packaged app's tests rather than pass silently.Review outcome (required — see AGENTS.md)
/self-reviewagainst the parent branch, judgement pass run with fresh context (the diff and the review standard only).npm run lintclean, unit suite green on both Node runtimes before and after (1178, then 1182), journeys 18/18, packaged smoke 9/9. 5 [fix here] · 2 [follow-up]; the five and one of the two are in the second commit.Applied
parsePatchFilesmarkshasBinaryDataandplanApplylists only the data-less ones. Found on the way: jsdiff swallows a section with no hunks (a binary, a pure rename, an empty file) whenever another section follows it, so a binary placed before a text file was already missing from the preview;parsePatchFilesnow parses section by section oversplitPatchSections, and the file list is the section list. Test for the swallowed shape.windowsArgs(agit configread each on Windows). The prefix is resolved once and passed down, and the per-section pass stops at 20 files with a line saying how many were not checked one by one; thenotAppliedanswer needs every section checked and says nothing past the cap..diffcarries binary data; GitHub's diff media type does not. It saysgit diff --binarynow.rollbackleaves a file whose bytes still match the snapshot alone, so a file Git never reached keeps its mtime and an edit made between the check and the write is not overwritten.Follow-up, not here
---/+++header heuristic inrewritePatchPathsandsplitPatchSectionsis line-local: a hunk that removes a line starting--right before one adding a line starting++would be read as a header, giving a refusal with a nonsense reason (never a bad write). Tracking hunk state (@@counts) closes it; noted in Disabled ticket-card buttons explain themselves through title, which Chromium hides and assistive tech skips #409's area of small follow-ups.Rules the standard does not cover (left for a later edit of the instructions file): a handler pair that promises then performs (
git:preview-patch/git:apply-patch) is reviewed as one unit, so a capability the writer gains the previewer gains too; a Git spawn inside a loop over user-supplied input needs a ceiling and a hoisted per-directory prefix; when an implementation is swapped for a tool, enumerate the old implementation's early returns and confirm each still has a test.Checked and sound:
notAppliedequivalent to or stricter than the oldpatchIsAbsent; both ends of a rename in the snapshot; the sandbox is Git's (../refused with 128, symlinks refused, patch text on stdin,mapToSrcLayoutonly ever prependssrc/); one primitive onrunGitwithwindowsArgsand nothing parsed from its output;legacySiteBlockstill gates the handler; norequire('isomorphic-git')undersrc/and the package still shipped through@wp-playground/storage, as the body says.Screenshots: none; the panel and its sentences are unchanged. The terminal line changes from the old "Applied N files." to the same text after Git's own check.
🤖 Generated with Claude Code
https://claude.ai/code/session_01J6koA9mvjSExVaKJp4nvSx