Changes touching this path

  • Extract the hunk-attribution engine into a deep `hunks` module The line-redistribution algorithm behind `loot absorb` (#399) lived as five free functions (diff_hunks, apply_hunks, attribute_hunk, split/join) plus LineHunk/HunkTarget in workspace.rs, testable only through the ~200-line Workspace::absorb via a full Workspace. Move it to a new `hunks` module — the write-side twin of `blame` (the read-side line-ancestry engine they share `lcs_match`/`Attr`/`split_lines` with). The whole line algebra becomes private behind a small bytes-in/bytes-out interface: `attribute(parent_bytes, working_bytes, owners, index_of) -> (Option<PathHunks>, Vec<Stay>)` builds a per-path plan, `PathHunks::apply_at(i, base_bytes)` rebuilds one ancestor's content, and `PathHunks::targets()` reports where hunks landed. `absorb` never sees a LineHunk or a line vector now — only oids, trees, and sealed bytes; it keeps just the chain-walk and the sealing/superseding it alone can do. The payoff is testability: the splice math gets direct unit tests over crafted byte inputs (the three migrated line-function tests plus interface-level tests), instead of only through absorb's full-Workspace integration tests. `blame:: split_lines` is made pub(crate) and reused, deleting the `split_content_lines` duplicate. AbsorbStay becomes `hunks::Stay`. Behaviour-preserving: the three absorb integration tests pass unchanged. The Phase A/B rewrite is outcome-identical (the in-effect emptiness check now lives inside apply_at). 38525b23 · dbf3dbe6…
  • Refit: plan/execute split for the change-rewrite verb family (#659) f2fdf6ed · dbf3dbe6…diff
  • blame grows six options and the one that mattered was -w, because ignore whitespace when attributing a line reads like a second comparison and this crate has exactly one line matcher: MatchOpts is a parameter of lcs_match_with, lcs_match is defined as that function at the default options rather than as a copy of its body, and the only place two lines are ever compared is match_key inside it, so textdiff, blame and hunks stay byte identical and the whitespace rule could reach diff tomorrow without anything gaining a compare of its own. -L slices a FINISHED annotation rather than narrowing the walk, which is why a range costs exactly the object reads a bare blame costs and the 98 content-address collapse still fires under it, and its rows carry the line numbers of the file rather than of the slice. --since bounds the walk by authored_at, which ADR 0043 makes advisory and never an ordering input, so two rules keep an advisory clock from dropping history silently: a change recording NO clock never cuts the walk, and the first version past the bound is KEPT as a boundary, marked with a caret, so a line older than the bound says the walk stopped instead of being re-attributed to the oldest change the bound happened to keep. --ignore-rev and --ignore-revs-file send a line to the next OLDER version of the path, marked with a tilde, which is a list filter over the versions and not the nearby-line guess git makes there; an unreadable ignore file is REFUSED rather than read as empty, because that failure is silent and inverted, the annotation that comes back blaming exactly the change the operator wrote the file to skip. the ticket got one criterion wrong and it is the porcelain one: it asked for the contract version on that channel like the other machine shapes, but the other machine shapes do not do that, ADR 0023 settled in its 1540 amendment that json carries contract and porcelain stays header-free, and the sentence claiming otherwise is one ADR 0066 records as false by design and guards off the shipped surfaces. so both channels ship and the version rides json, and the spelling is --porcelain rather than -p because sixty verbs answer to one pair of machine selectors. sealed is pinned under every flag rather than once, and --since was the hardest of them: a bound tight enough to stop short of the seal legitimately yields no sealed row at all, so what it must not do is NAME those lines, and it does not (#1516) b8cb568b · 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.