You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
The overlap check compared raw header numbers, ignoring git's convention that a side with
no lines reports the line before its empty range. A hunk ending in a pure deletion —
a file whose tail is removed — therefore looked like an overlap, and select '*' refused
a diff git itself writes: exit code 70 on roughly one in ten ordinary diffs (measured over
200 generated cases). Present since 0.7.0, when the new-side check was introduced.
A full binary patch (git diff --binary) lost its structure: the payload lines
(literal <n> and the base85 body) were filed as leading headers and emitted above the GIT binary patch marker, so git apply rejected the result as garbage while hunkpick
exited 0. This is the form needed to stage a binary change, and the binary marker of a
CRLF diff went unrecognised for the same reason.
A sub-hunk with no old-side lines (an appended block) carried a header one line past
where git puts it (@@ -4,0 +5 @@ instead of @@ -3,0 +4 @@), and the new-side anchor
inherited the shift.
An @L slice of a whole-file deletion produced a self-contradictory patch: the header
declares the file removed while the body keeps the unselected lines as context, which git
rejects (deleted file f still has contents). Such a selection is now a usage error.
A combined diff (diff --cc, @@@ headers — what git writes for a merge) was read as a
two-sided one: the hunk body was truncated at its first line and a --- removed in both
line invented a file entry. It is now rejected as unsupported (exit 2) instead of losing
data silently. See docs/ADR/0012-two-sided-diffs-only.md.
The listing gave a CRLF diff's @@ header a separating space and a raw CR — inside a JSON
string field — where emit already knew the CR is the line ending, not section text.
A context line for an empty source line is a lone space, and transports that strip
trailing whitespace deliver it as a zero-length line. Parsing treated that line as the
end of the hunk, dropped the rest of the body into the file's headers and emitted a
diff git apply rejects as garbage — with exit code 0. Such a line is now read as
context and emitted with its marker restored.
Lines that follow a hunk body — a blank separator between hunks, the -- signature git format-patch appends, a stray Binary files ... marker — were emitted with the
leading headers, which moved them above the first @@. They now keep their position,
and the binary marker is no longer dropped silently.
split replaces one hunk with several but left those trailing lines at their old
positions, so a signature recorded after the last hunk was emitted between the pieces. git apply rejected the result (patch fragment without header) while hunkpick exited 0.
Each line now follows the hunk it followed before the split.
Emitting those lines rescanned the whole list for every hunk, which is quadratic in their
number: a 7 MB diff carrying a separator after each of its 128 000 hunks took 15 s to
re-emit, against 0,2 s to list. One pass now walks the list alongside the hunks (0,18 s on
the same input).
A binary file, a pure rename and a mode-only change have no ---/+++ lines, so they
had no path and could not be addressed in a multi-file diff. Their paths are now read
from the diff --git line, and * takes a hunkless entry whole.
Paths quoted and C-escaped by git ("a/\303\251.txt", the core.quotePath default for
non-ASCII names) are decoded, so a selector spelled with the real file name matches.
The emitted diff keeps the original bytes.
A CRLF diff left the CR in the file path (breaking path: selectors) and added a stray
space to the hunk header, so the round-trip was not byte-identical.
A deleted file was listed as /dev/null; it is now shown under its old name.
Line numbers near u32::MAX from the input header overflowed while checking hunk
overlap: a debug build panicked with exit 101, a release build wrapped and decided the
check on a meaningless value. The bounds are computed in u64, and sub-hunk starts
saturate rather than wrap.
A defect of the input diff (a header that disagrees with its body, e.g. a truncated
diff) was reported as a verification failure of hunkpick's own result: exit code 70 and
a Debug dump of internal fields. It is now a usage error (exit 2) in prose, with the
sub-hunk numbered from one as list numbers it.
A reader that closes the pipe first (hunkpick list | head) ended the run with exit 74
and a Broken pipe diagnostic; it is now a normal end of work (exit 0). Both this and the
exit-code change above are recorded in docs/ADR/0013-exit-code-contract.md.
In the path:set selector form a broken set was re-read together with the path, so f:2-1 was reported as not a number: f:2 instead of reversed range.
split now recomputes new-side anchors like select does, so both commands treat a
diff carved out of a larger one the same way.
In a plain (non-git) diff a header-only entry absorbed the next file's marker lines.
The human listing escapes text a terminal would act on (escape sequences, control
bytes, bidirectional overrides) instead of passing it through.
A file whose name is not valid UTF-8 (legal on Unix) could not be addressed: the
argument was refused before hunkpick saw it. Selector paths are now taken as raw bytes,
so such a file is reachable by name; only the set after the : must be ASCII.
A selection that did not include the input's last line inherited its "no final newline"
flag, so the result ended mid-line and git apply called it a corrupt patch. The flag now
travels only when the result does end on that line.
A hunk header carrying a token hunkpick cannot represent — @@ -1,3,9 +1,3 @@, @@ -1,3 +1,3 junk @@ — was parsed as if the extra part were absent and emitted without it,
at exit 0. Both are now parse errors (exit 2).
A diff saved in UTF-16 (what git diff > patch.diff writes in Windows PowerShell 5.1) was
reported as "binary input: NUL byte found", which points at the wrong thing. A UTF-16/UTF-32
byte-order mark is now named, with the re-encoding command to fix it.
A git that could not be started for --verify-result-diff-git was reported as a failed
verification of the result diff (exit 70). The check never ran, so it is an environment
failure: exit 74.
Output written after the last newline could be lost silently: stdout is line-buffered and the
runtime's implicit flush at exit discards its error. It is flushed explicitly, and a failure
is exit 74.
list --json did not end its output with a newline.
A combined diff whose entries carry no ---/+++ pair (a file resolved the same way in both
parents) was reported as "no diff markers found" rather than as the combined diff it is.
Changed
Auto-splitting a hunk is linear in the number of change runs; it re-counted the whole
prefix per sub-hunk before. On a 1.9 MB one-hunk diff with 64 000 runs list went from
3.9 s to 0.06 s.
split no longer clones the whole parsed diff to rewrite one hunk.
The git apply --check child process no longer inherits GIT_DIR, GIT_WORK_TREE and
related variables, so -C DIR alone selects the repository.
--color is described in list --help; the exit-code table in the README distinguishes
SIGINT (130) from SIGTERM (143) and documents the closed-pipe case.
Release archives carry THIRD-PARTY-NOTICES.md with the license texts of the crates
linked into the binary, and the release pipeline builds and verifies every archive
before the irreversible cargo publish rather than after it. The release procedure is
written down in RELEASING.md.
README states the measured peak memory (6x–19x the input, depending on average line
length) instead of "a few hundred MiB", so the input limit is not read as a RAM ceiling.
The crate is built on the Rust 2024 edition, formatted with the matching style edition.
The minimum supported Rust version is unchanged (1.85, the release that stabilised the
edition), so nothing is required of consumers; the dependency resolver now honours that
minimum when picking versions. See docs/ADR/0011-rust-2024-edition.md.
path: selectors resolve through an index built once per invocation instead of scanning the
file list per selector: 16 000 selectors over 16 000 files went from 1.5 s to 0.04 s.
--verify-result-diff-git documents what it actually checks — the working tree, which in the
usual staging pipeline already holds the edits, so the flag reports a correct result as not
applying. Same for the JSON listing, whose text fields are the diff's own content: not
display-sanitised the way the human listing is, and lossy for non-UTF-8 bytes.
The selector index limit (2^20 per selector) is named in the error message and in the README.
The release pipeline checks the tag against the manifest, both lockfiles and the CHANGELOG in
a job of its own, before anything is built, and a release can be rehearsed by running the
workflow with no tag named, which reads the version from Cargo.toml and publishes nothing.
Behavioural decisions of this release are recorded as ADRs: docs/ADR/0012-two-sided-diffs-only.md
and docs/ADR/0013-exit-code-contract.md.
Added
Generated tests, because the defects above were found by generating inputs rather than by
reading code: tests/differential.rs compares hunkpick with real git over generated diffs
(a selection applies, staging one sub-hunk at a time converges on the target, the output is
valid input for the next invocation), tests/property.rs uses proptest for the forms git
will not produce on demand (CRLF, a missing final newline, a mail preamble), and fuzz/
holds libFuzzer targets for parsing, the parse . emit fixed point and selector handling,
with committed seeds (fuzz/seeds/) and a token dictionary. CI builds every fuzz target on
each push; a scheduled workflow searches twice a week and keeps its corpus across runs.
Changed (library API)
Selector::File.path is Option<Vec<u8>> instead of Option<String>, and select::parse_selectors accepts anything convertible to OsStr (a &[String] still
works). This is what lets a selector name a file whose path is not valid UTF-8. select::resolve_file takes Option<&[u8]> accordingly.
select::build_view returns Vec<Vec<Hunk>>: the position in the result is the file
index, so the redundant index in each tuple is gone.
The crate denies undocumented public items (#![warn(missing_docs)]); every exported
item now carries rustdoc.
Line::no_newline is Option<Vec<u8>> (the marker as it arrived) rather than a flag, so a
CRLF diff keeps the marker's line ending.
Patch gained preamble (the lines before the first file entry — the mail head of a format-patch output, which used to be dropped while its footer was kept) and no_trailing_newline (an input that ended without one now leaves without one). Together
with the two above, emit now round-trips its input byte for byte, not just a
git-canonical diff.
ParseError gained a Combined variant for merge diffs.