Changes touching this path
- loot apply-patch no longer empties a renamed file: a rename stanza now reads its preimage from its old path, so a pure rename moves the old bytes unchanged and a rename with hunks applies them strictly to the old content, the whole run is refused naming the path when the old path is absent, binary or not the content the hunks were made against, or when the new path already holds other bytes, --check gives the same refusal, and the old path is removed only after every new path is written and not when another stanza of the patch writes it. a git copy row is refused by name, since format-patch writes none and it used to write an empty file at the copy path. before the change a pure rename exported by format-patch applied as an empty new file with the original deleted and exit 0, a rename with an edit failed with a garbled error, and the first seven spawned-binary tests went red (0 passed, 7 failed); with the pure rename reading the new path (5 passed, 3 failed), the new-path check removed (7 passed, 1 failed), an absent old path read as empty (7 passed, 1 failed), the old-path binary check removed (7 passed, 1 failed), the hunks ignored (6 passed, 2 failed), the copy refusal removed (7 passed, 1 failed), the old path never removed (5 passed, 3 failed) and the guard for an old path the patch also writes removed (7 passed, 1 failed). the existing format-patch and apply-patch round trips stay green, the split literal in the conflict error is joined, ADR 0082 section 2 and CONTEXT.md state the rename rule, and the workspace suite is green (#1995)
cb93ea4d · dbf3dbe6… - loot apply-patch closes the preimage gaps #1992 and #1995 left: an add is refused over a path holding bytes other than the ones it adds, which it used to write in front of them, a modify stanza with no hunks is refused, which wrote an empty file at an absent path, a deletion of an absent path is refused rather than reported as done, and a patch that writes or deletes one path in more than one stanza or renames one old path twice is refused. the similarity index, dissimilarity index and index rows are read past, so a git format-patch -M rename applies as a rename where it used to write an empty file at the new path, and another unread row above hunks is refused by name. apply_strict now reads a deleted last line without the no-newline marker as a claim that the file ends with a newline, the deletion arm compares through apply_strict, and the writer no longer discards a removal error. ten new spawned-binary tests went red before the change (deletion 5 passed, 2 failed; preimage 0 passed, 4 failed; rename 8 passed, 4 failed), a new pin for a deleted file that grew a line holds the unified deletion check, and over the apply-patch and format-patch test files the pins went red with the add check removed (46 passed, 2 failed), the hunkless modify refusal removed (47 passed, 1 failed), the git rows unread (47 passed, 1 failed), the unread-row refusal removed (46 passed, 2 failed), an absent deletion planned as a delete (47 passed, 1 failed), the shared path check removed (47 passed, 1 failed), the shared rename source check removed (47 passed, 1 failed), the newline claim removed (46 passed, 2 failed) and leftover lines ignored by the deletion check (47 passed, 1 failed). the format-patch round trips stay green, ADR 0082 gains section 4, one table of what each stanza class checks, its rename paragraph no longer claims more than the hunks compare, CONTEXT.md states the rules, and the workspace suite is green (#1997)
15e2d13e · dbf3dbe6…diff - loot apply-patch refuses a patch that deletes a path and also renames it away: refuse_shared_paths now counts every path a stanza removes, as a deletion or as a rename old path, and refuses both rows, where the run used to move the file, remove the old path once and discard the second NotFound, and a row it refuses that was already refused keeps its first reason with the shared one appended. a rename old path that another stanza writes stays allowed, as #1995 pins, and ADR 0082 says why and where git apply differs; the rename writer now reports every removal error. the offer_cost double counts account_of, and the two constant-cost cases pin it at the one read Reader::new makes since #1955. the apply_strict comment and ADR 0082 no longer say an unmarked kept last line claims nothing: format-patch writes one when only the new side ends with a newline, so it stays unchecked, and a hunk that adds no line loses that newline. plan_one shares one helper for the rename and add verdicts, the ClassReads, ingest_cost and offer_cost doubles and Reader::new have docs true to the code, no shared base double is added since the conformance double delegates every method, and the CONTEXT.md, ADR 0077 and ADR 0082 rewrap leftovers are fixed. before the change the new rename test went red (12 passed, 1 failed), as it did with deletions uncounted, with the deletion refusal removed and with the rename refusal removed (12 passed, 1 failed each); the kept-reason test went red with the first reason overwritten (8 passed, 1 failed); the account pins went red with a per-object account_of read in readable_rows (11 passed, 2 failed), which the old double let pass (13 passed), and with the owner arm reading nothing (11 passed, 2 failed). the workspace suite is green, and the loot-forge suite is green against a throwaway cluster with the CI env block and LOOT_FORGE_TEST_REQUIRE_DATABASE (#2006)
f16115c2 · dbf3dbe6…diff - loot apply-patch no longer deletes the file on a case-only rename where the filesystem folds case: the writer now compares a rename old path with the paths it wrote by file rather than by name, so a rename from A.txt to a.txt renames the old path on disk, which keeps the bytes and gives the last name component the new case, where it used to write a.txt, which was A.txt, then remove A.txt and exit 0 with no file left, and a rename with an edit whose new path is its old file is no longer refused as a new path already holding bytes. refuse_shared_paths keys a path that exists by its file, the device and inode on unix and the canonical path elsewhere, and a path that does not exist by its name, lowercased when the root reads .LOOT as its .loot, so two stanzas whose paths differ only in case, or two renames to such new paths, are refused there and the refusal names both spellings, and an old path kept because another stanza writes it is kept whichever case that stanza spells it with. on windows two hard links still give two canonical paths, so the new hard-link pin runs on unix only. before the change the case-only rename lost the file through the spawned binary and the rename tests went red (13 passed, 4 failed); they went red with the case-only rename removed from the writer (16 passed, 1 failed), with the writer comparing by name (16 passed, 1 failed), with the new path of such a rename read as a file (16 passed, 1 failed), with the case probe never folding (16 passed, 1 failed) and with the shared paths keyed by literal name (15 passed, 2 failed); with the file key skipped they stay green here (17 passed), since the case fold answers the same on this filesystem. the case-sensitive halves ran green under a directory marked case-sensitive (17 passed). ADR 0082 section 4 states the aliasing rule and its limits, CONTEXT.md names it, the unix file id is spelled as paths so the destination census does not read it as a host, and the workspace suite is green (#2007)
f7c20944 · dbf3dbe6…diff - loot apply-patch no longer keeps the old name of a rename onto a hard link: #2007 renamed the old path onto the new one whenever the two were one file, which rename(2) does nothing for between two hard links, so the old name stayed and the run exited 0. the writer now renames only when the two paths are one directory entry, which on windows is two paths with one canonical path and on unix two names of one inode that differ only in case and are not both listed in their directories, and otherwise writes the new name and removes the old one by name, and an old path is kept only when a row writes that directory entry, so a rename from a hard link of a path another stanza writes removes the old name too. a stanza whose path or rename old path is or passes through a symbolic link is now refused, checked component by component with symlink_metadata, since a write through a link lands at its target and the file identities follow links. PathKeys and PathKey replace Names and Alias, refuse_shared_paths asserts one row per stanza, the removed-twice refusal words are one const, honor_burn says it is pub(crate) only for the server.rs agreement test, and the purge.rs, ingest.rs and ADR 0082 rewrap leftovers are fixed. the Rotate card on the account page now says a leaked key stops pushing and proposing rather than writing, that the retired-key refusal covers burns, and that this forge does not check retirement on every signed request, naming withdrawing an authored proposal beside revoking a grant as examples, and its JSX comment states the forge-before-site deploy order ADR 0038 gives. the new hard-link rename tests pass on windows, where the old code already passed them, and their unix red-first has not run here; with the unix entry rule compiled on windows the rename tests stay green (20 passed) and go red with the new name read as listed (18 passed, 2 failed), the windows rule answering two entries goes red (18 passed, 2 failed), and the symbolic-link test, which uses a junction here since a file link needs a privilege, goes red with the refusal disabled (19 passed, 1 failed) and with only the last component checked (19 passed, 1 failed). the unix code type-checks for x86_64-unknown-linux-gnu in a scratch crate, ADR 0082 section 4 and CONTEXT.md state the entry rule and the link refusal, the site gate is green and the workspace suite is green (#2014)
ab00ecd2 · dbf3dbe6…diff - cherry-pick, revert and a surface that settles a stale-disk debt no longer destroy a case-only rename where the filesystem folds case, and the first no longer records the loss: #2011 routed two tree writers through loot_core::disk_entry and wrote that every writer which removes a path it may just have written asks that module, while two that did not were sitting in the tree as the sentence was written. Workspace::apply_change_delta, the one core cherry-pick and revert share, walked the merged actions as a BTreeMap and so wrote A.txt and then removed a.txt by name, which where case folds is the file it had just written, and it snapshots, so loot revert over a recorded case-only rename reported both paths converged over an empty directory and minted a change recording 1 deletion. Workspace::stale_debt_the_surface_left, the one scoped deletion a surface is allowed, compared the written set by name, and a case-only rename preserves the bytes, so the digest guard that exists to protect the operator matched the file the surface had just written: loot surface printed the path it had surfaced, left the directory empty, and the next loot status read it as deleted, which is the reproduction #2011 declined on the false reading that a surface prunes nothing. Each was reproduced by hand through the spawned binary first and then as a new spawned pin, and the pair went red over an empty listing before the change (4 passed, 2 failed) and is green after (6 passed); with the delta writer put back to removing by name they go red again (5 passed, 1 failed) and so does the new census (0 passed, 1 failed), and the stale-debt prune put back reddens the same two with the same counts. Both writers prune through prune_unwritten now. Which working-tree removals are weighed stops being a sentence: workspace.rs carries a census that reads every removal joined onto a working-tree root out of loot-cli/src and loot-core/src, names what each one removes and what the walk cannot see, and it named the offending function under each of those two mutations, went red at its own classifier control with ROOTS emptied and went red with a name dropped from its expected set (0 passed, 1 failed each). one_entry refuses a path that is or passes through a symbolic link now, since file_id follows links and on Windows two paths with one id were otherwise one entry, so fs::rename would have moved a link over the file it points at; the check is the symlink_on apply-patch already made, moved into the module, pinned with a junction here and red without it (5 passed, 1 failed). The hard-link pin names its links A.txt and a.txt so the fold map reaches the entry rule at all, where old.txt and new.txt never called it, and it skips a filesystem that folds case with a note; it stays unix-only and its red-first has not run here, though the body type-checks and runs on windows with the cfg lifted (7 passed). The four case probes become one loot_core::disk_entry::folds_case, and with it answering no the entry pin and two apply-patch case pins go red (5 passed, 1 failed; 18 passed, 2 failed). enclosing_fn moves into source_walk beside the #1929 census that asked it first and strips any visibility, after pub(super) made the new census name the wrong function. ADR 0022 carries the correction and its prunes-nothing claim is gone, ADR 0082 section 4 states where the probe and the link refusal live, CONTEXT.md names both writers and the census, the surface.rs and materialize_target notes stop claiming the two prunes answer the same set of failures, and the rewrap leftovers in ADR 0022, CONTEXT.md and the account page are fixed. The perf gate dry run reads every counter the same before and after and says no move both times, the site gate is green, and the workspace suite is green (3824 passed) (#2018)
baec4112 · dbf3dbe6…diff - review sweep 15 fix-up: the evolog dash is on an empty FIRST LINE, the subject rule now has one home, and the surface prune says plainly that it discards. loot evolog --porcelain dashes its last column when the message first line is empty, which an empty message and a message whose body starts under a blank one both are, and three places said an empty message: the doc above the encoder, the ADR 0023 amendment of 2026-09-17 and CONTEXT.md. The wording moved rather than the bytes, because dashing on an empty message instead is a porcelain byte change for a message that opens with a newline, and a new spawned pin, a_message_whose_first_line_is_empty_is_the_dash_too, puts a run under the sentence: the file reads 5 passed, and with the whole message put back in the column it is 2 passed, 3 failed, the single-line control among the two that stay green. render::subject_line was billed as the one home while six call sites derived that first line inline, the four the ticket named plus the shortlog author rollup and the missing-object listing, so log --oneline, heads, show, range-diff and those two all ask it now, and the near-cousins that each do something more, the note ellipsis, the revert subject and the loot-first PR title, are named beside it rather than routed. The control is the spawned binary: 28 outputs over two repos, one carrying a multi-line message and one a blank first line, are byte-identical before and after, and with subject_line returning the whole message 10 of 11 of them move and the loot-cli lib suite reads 1219 passed, 4 failed, naming the shortlog rollup and the oneline row among them. The surface stale-debt prune keeps the removal error it discards, which #2018 declined and its closing note then reported as fixed, and the doc above it now says the let _ is deliberate, that apply_change_delta propagates where this one does not, and why: the delta writer snapshots, so a refused removal would be recorded as a deletion the disk never took. The removal census in workspace.rs states what its matcher is blind to, a type-annotated binding, a call split over lines and a path derived from a root-joined one, and names disk_entry::drop_empty_parents, whose remove_dir climbs from a dest its caller joined and which the fold cannot reach, because remove_dir takes an empty directory only and a directory holding a file the writer just wrote is not empty; widening the matcher would not have reached that call, so the blindness is stated instead. CONTEXT.md gives that census its crate and spelling scope. migration_serial records that hold blocks and is not re-entrant, and that it serialises migration runs rather than pg_authid, naming the fixed-name CREATE ROLE in tests/rls.rs that writes the shared catalog outside it; Held loses its Option and its Drop impl, since dropping the field is what the compiler already does. The migration census compares a BTreeSet the way the removal census does: with the const reordered it stays green where the Vec compare it replaced goes red (0 passed, 1 failed), and a name dropped from the const reddens it (0 passed, 1 failed). pg/tests.rs cross-references its own source walk and fn finder to source_walk in loot-cli and states where the pair differs. The folds_case helpers in two test files become folds_case_under, so neither shadows the disk_entry rule it asks. The rewrap leftover in account.tsx that #2018 moved rather than fixed is rewrapped with its neighbours, and so is a second one mid-paragraph in CONTEXT.md that the same rewrap reached. The forge suite is green against a throwaway 17.0 cluster (469 passed over nine binaries, no skip note) and its lock case goes red with the session dropped rather than held (0 passed, 1 failed), the site gate is green (643 passed, every surface under its ceiling), and the workspace suite is green (3833 passed) (#2023)
c38e3299 · dbf3dbe6…diff
Renames are not followed. loot's tree maps a path to an address, so a rename is a delete and an add. This list is the history of the name, not of the bytes.