Releases: VitalyOstanin/hunkpick
Release list
v0.10.0
Added
FileDiff::tailandFileDiff::ends_on_a_trailer_linesay which trailer lines follow an
entry's last hunk, and what having one means for the line the entry ends on.splitand
selecteach spelled that rule out for themselves, and the two spellings were free to drift.parser::reads_as_a_diff_lineanswers whether a line is one a diff writes: a marker line
carrying what its marker promises — for@@, a hunk header that parses — or an extended
header (index,similarity index,rename from, the mode lines) carrying what stands after
it. A rename and a mode-only change carry no---,+++or@@, so past thediff --git
line those headers are all such an entry has. The four a path follows (rename from,
rename to,copy from,copy to) are read on their own head, because a path may be in any
script; what follows the others is an object name, a mode or a percentage, written in ASCII
wherever the patch was, so a bareindexsays as little as a bare---. A caller weighing a
line out of a stream with no byte-order mark asks about the diff format, and the answer lives
next to the code that reads that format rather than as a length beside the check.parser::markersreports every marker a diff line may open with, ordinary and combined.
#[doc(hidden)], likecliandgitenv: the binary is a separate crate, and its tests of
the UTF-16 check weigh a corpus against each marker the parser knows, which they otherwise
write out a second time.parser::headers_ascii_followsreports the extended headers whose line is ASCII past them,
the ones weighed the way a marker is.#[doc(hidden)], for the reasonparser::markersis:
the binary's test that two bare headers say nothing named three of the seven, and was free to
keep naming three as the list grew.parser::headers_a_path_followsreports the four extended headers read on their own head.
#[doc(hidden)]for the same reason: the binary's test of what the count of two costs weighs
those four, and named them a second time to do it.parser::the_header_a_path_follows_inreports which of the four headers a path follows opens
a line, the longest of them where one opens another.#[doc(hidden)]for the reason
parser::markersis: the binary's UTF-16 check weighs such a header against the line it read
it out of, and a lookup written out there would be free to take the first that opens the line —
whose length then decides how much of the line counts as said past it.parser::carries_something_pastanswers whether a line says anything past a prefix beyond
whitespace — the one reading the parser gives both lists a line is weighed against.
#[doc(hidden)]for the reasonparser::markersis: the binary's UTF-16 check weighs a header
a path follows against the line it read that header out of, and writing "something past it" out
there would be a second reading of the same tail.
Changed
SplitError::OutOfBoundsnames what it addressed with the newsplit::Addressedenum instead
of a&'static strholding"file"or"hunk". A library caller can now match on the two
exhaustively; the message is unchanged.gitenv::insulated_gitbuilds the insulatedgitinvocation that both test layers were each
building for themselves; it also pins the message locale, which neither copy did.model::TrailerLinenames the position-and-line pairFileDiff::trailerholds, which three
signatures spelled out. The field's type is unchanged.gitenv::MESSAGE_LOCALE_VARSpairs each variable with the newgitenv::LocaleActionsaying
what happens to it, instead of naming the one dropped variable a second time in a constant of
its own. A rename in the list and the constant left behind no longer disagree in silence.
Fixed
- Binary data holding a NUL-padded diff marker was answered with an
iconv -f UTF-16LEfor a
stream that is not UTF-16. Three code units spelling@@, or four spelling---, are a
diff marker, and every rule tried against them read a single line: a minimum length, then the
length of the marker that matched, then what the marker promises — under which@@ -1was
refused as no hunk header while--- xwas accepted as a path, though both are the same
accident of the bytes a PNG holds. What a stream is asked for now is a second line: two lines
of the opening window have to read as lines a diff writes. A patch has them — an entry carries
a header and a hunk, a rename carries the lines naming both paths — and binary data does not.
Nothing is asked about the stream around those lines, so the header order of a mail, a patch
opening with a blank line and a path in another script stay free; each of them was once a
reason to answer a UTF-16 patch as binary input. - Binary data holding a NUL-padded
rename fromtwice was answered with aniconvall the
same. The four headers a path follows are read on their own head, because the path may be in
any script and the parser is handed the line already read up to the first unit that is not
ASCII — which leaves a barerename fromand a realrename from файл.mdthe same twelve
bytes. The check now weighs those four against the line it read them out of, where the path is
still there: the header counts where something stands past it, whatever script that something
is written in. The patch the reading was written for keeps both its lines, and a window that
holds nothing but the header twice keeps neither. - A
git format-patchmail written in UTF-16 without a byte-order mark was rejected as binary
input. The encoding was looked for in the opening 128 characters, which a mail spends on its
headers, so the diff marker the check waits for arrived too late. The window is now wide
enough for those headers, and text that is not ASCII no longer ends the reading before the
marker: a commit subject or message in another script is read past, and a path in another
script leaves the marker before it standing. Such a mail — and a rename, a mode change or a
binary file whose only marker line carries that path — is answered with the encoding to
convert from. Nothing about the stream around the marker line is asked for any more, so the
order git wrote the mail headers in and a patch that opens with a blank line no longer decide
the answer either. selectappended a newline to a diff that ends in an entry with no hunks — a pure rename or a
mode change — and arrived without its final newline. Such an entry is written from its headers
alone and is always emitted whole, but it was judged by its picks, of which it has none, so the
result was taken to end somewhere other than where the input ended.splitdropped the trailing-newline flag of a diff that ends in agit format-patch
signature. The signature is emitted after the last hunk, so the output still ended on the line
the input ended on, but the cut was judged by the last line of the last hunk alone:hunkpick split 1 --at 3on such a diff appended a newline the input never had.selecthas taken the
trailer into account since it learnt the rule;splitnow does the same.- A panic in one of the threads reading the child's output was reported as a panic in the thread
feeding its input:FeedError::WriterPanickedcovered all three threads, and the message a
caller saw named the feeding side whichever thread had actually died. Reading now has its own
variant,FeedError::ReaderPanicked, andGitCheckError::ReaderPanickedbehind it; both still
exit 70 as an internal error.
v0.9.0
Fixed
-
A hunk header whose ranges were separated by something other than one ASCII space — a tab, two
spaces, or a non-ASCII whitespace character such as U+00A0 — parsed, and the header was then
rebuilt on the way out with a plain space. A diff git calls a corrupt patch came back out as
one git accepts, at exit 0. Such a header is now refused as malformed (exit 2), the same as any
other header hunkpick cannot reproduce byte for byte. -
The sign of a hunk range was not checked at all:
@@ +1,3 +1,3 @@,@@ -1,3 1,3 @@,
@@ -+1,3 +1,3 @@and@@ -1,+3 +1,3 @@all parsed and were rendered back as
@@ -1,3 +1,3 @@at exit 0, while git reads every one of them as a corrupt patch. The leading
-and+are now required, and a range component has to be plain ASCII digits. -
Selecting by a content id shared by several sub-hunks scaled as the square of the number of
matches, in both time and memory. Reading every id out oflist --jsonand passing them back
— the documented batch flow — names such an id once per sub-hunk, and each naming both
appended a pick per match and re-ran the collision check over every match. A diff of 8 000
identical changes (150 KB) took 10 s and 3 GB of RSS; it is now a fraction of a second. The
same accumulator defect was reachable through a repeated*. -
The cap on how many indices a selector may materialise (
1048576) applied to one selector
while the number of selectors is bounded only by the length of the command line: 200 copies of
1-1048576— 2.6 KB of arguments against a four-sub-hunk diff — reached 1.6 GB of RSS before
any index was compared against the diff. The cap is now one allowance for the whole
invocation, and its message says so. -
splitnow applies the same trailing-newline rule asselect. A diff that arrived without
its final newline keeps that property only while the result still ends on the line the input
ended on, and a cut that drops the piece holding the tail does not:hunkpick split 1 --at 3
on such a diff ended its output mid-line at exit 0, andgit applycalled the result a
corrupt patch.selectwas taught this in 0.8.1;splitwas not. -
The defect a scheduled fuzzing run found — a hunk header declaring six old lines over a body
of three, whose selection then failed the tool's own output check — was closed by making the
fuzz target skip such input, and nothing recorded it afterwards. The precondition is now
stated onselect, the crate example screens its input the way the CLI does, and
select_carries_an_inconsistent_header_into_its_resultholds the shape, which until now
existed only in one machine'sfuzz/artifacts/. -
The
selectorsfuzz target recounts a hunk header from its body instead of skipping the
input. Skipping cost roughly a third of the parsed stream — the mutations libFuzzer favours
break the header-to-body correspondence more often than not — so most of what it generated no
longer reached the selection code the target exists for. -
Fuzz seeds are checked by
tests/fuzz_seeds.rs, which found three that had stopped reaching
the code they were written for:simple-content-idnamed a made-up id,two-files-path-index
named a path absent from its own diff, andtwo-files-starused a bare*on a multi-file
diff, which is a usage error. Each was silently skipped by the target rather than reported. A
seed for the@idcollision path was added, and the byte-identicalparse/androundtrip/
seed directories were merged into onediff/. -
The property tests reach two shapes they could not generate before: an edit whose changed
lines repeat (so several sub-hunks share a content id, and a selection by that id is checked
againstid_countfromlist --json), and agit format-patchsignature after the last
hunk (so the trailing-line shift insplit_file_hunkis covered by something other than one
example). -
Fuzzing is run from
scripts/fuzz-all.shandscripts/fuzz-repro.shinstead of a command
copied out of the guides. The documented command was missingmkdir -p fuzz/corpus/<target>,
and the corpus directory is gitignored, so on a fresh clone libFuzzer refused to start; the
scheduled CI run had already failed on exactly that and gained the step, while the guides did
not.fuzz/seeds/README.mdalso omitted the nightly override and the explicit target triple
that CONTRIBUTING.md calls mandatory, and the local command had no-timeout=10, so one hung
input could swallow a short run's whole budget. The scripts carry all four, and a test checks
that every script the documentation names exists and is executable. -
Documentation no longer contradicts the tool. Two README examples still recommended
--verify-result-diff-giton agit diff | hunkpick ... | git apply --cachedpipeline after
the README gained the paragraph explaining that such a command reports a correct result as a
failure (exit 70); both now show a patch file against a tree at the pre-patch state, and a test
reads the shell blocks of README.md and CONTRIBUTING.md so the shape cannot come back.
hunkpick --helpclaimed "every subset applies" for@Lwhileselect --helpand the README
already listed the two exceptions. The README described the selector index ceiling as per
selector, which it no longer is, and called two of the four lossy JSON text fields lossy. -
Only git's own verdict now decides exit 70 for
--verify-result-diff-git.git apply --check
answers 1, and only 1, when a patch does not apply; every other status is git failing before it
looked at the diff. A repository git refuses to read (fatal: bad config line …, exit 128) was
reported asgit apply --check rejected the result diff, and a git killed by a signal — the OOM
killer, a timeout — produced the same sentence with nothing after the colon. Both now name the
status and exit 74, so exit 70 keeps meaning what ADR 0013 reserves it for: a result hunkpick
itself produced. -
-C DIRnaming something that is not a directory is reported against that argument. A mistyped
path reachesCommand::spawnas the sameNotFounda missinggitbinary does, so both printed
failed to run git: No such file or directory (os error 2)at exit 74 and sent the caller after
their git installation instead of after their own typo. The path is now checked before git is
started and named in the message at exit 2; if git still fails to start, the message says which
directory it was to run in. -
The one non-ASCII character in hunkpick's own output is gone. The encoding diagnostic added in
this cycle carried an em dash, and it is addressed by construction to a Windows user, whose
console on cp866 or cp1251 shows it as mojibake in the middle of the sentence. The guard added
alongside it covered the help texts only, so the character came back in an error message the
same week; it now covers every string literal ofsrc/outside the test modules. -
One copy of the plumbing that runs
gitwith a diff on its stdin.gitenv::feed_and_wait
replaces three near-identical blocks — the result-diff check and two test helpers, two of them
added last cycle — that had already drifted apart in how they treat a failed write. It also
closes the failure path they shared: the child is now kept in hand and killed if waiting on it
fails, becausestd::thread::scopeparks until every thread it started has finished, and the
writing thread can be blocked in awritenobody is going to read. The failure
was a hang rather than a diagnostic, and the fallback test command documented in CONTRIBUTING
has no per-test timeout. The stub file names that keep a developer's git configuration out of
the test repositories moved togitenv::insulate_config, next to the list of variables that
was consolidated there last cycle, andtests/git_config_isolation.rsnow poisons
GIT_CONFIG_SYSTEMas well asGIT_CONFIG_GLOBAL, so dropping either one turns the suite red. -
The crates.io token no longer reaches
cargo publish --dry-runin the release workflow. A dry
run never asks the registry for credentials, but it does run a verification build, which
compiles the package along with the build scripts and proc macros of its dependencies — all of
them inheriting the step's environment. A rehearsal (workflow_dispatchwith an empty tag)
builds whatever branch the dialog names, so the step handed a credential good for every crate
of the account, and outliving any access to this repository, to code from an arbitrary branch.
The publishing step keeps its own copy of the token. -
The release pipeline checks the public API too.
cargo semver-checks check-releasewas a CI
gate only, and CI runs on branch pushes: a tag on a commit CI never saw, or one whose version
was bumped after the last run, reached the irreversiblecargo publishwith the API unchecked.
The release workflow now runs the same gate for the same reason its test, lint and MSRV jobs
are duplicated there. -
The documented local loop covers what CI gates. The
fuzzdirectory is a workspace of its own,
so the--allincargo fmt/cargo clippynever reached it, while CI lints it as a separate
step: a fuzz target edited locally passed every documented command and failed the pull request.
The two commands are in CONTRIBUTING.md now, along withcargo-semver-checksas a tool the
project expects; RELEASING.md runs the semver and MSRV checks before the release commit, and
the README says its list is the everyday loop and points at CONTRIBUTING.md for the full set
rather than claiming to be it. -
RELEASING.md describes the pipeline that exists. Its table of jobs still began with
testand
attributed the tag, manifest, lock and changelog checks topublish, while they have their own
first job (verify-metadata) that every other job waits on — the same document said so three
sectio...
v0.8.1
Fixed
- The two-round
@Lexample inselect --helpand in the README could not be run: it reused
the first round's line numbers (1@L91-120) after a re-diff that renumbers them, so the
second command failed with exit 2. Both now show the corrected round and say why the numbers
change; an integration test drives the selectors straight out of the help text.
Changed
- Reading a diff from a terminal — no pipe, no
-i— now says so in one line on stderr before
it blocks. A forgotten pipe used to be indistinguishable from a hang. Nothing is printed when
stdin is not a terminal, so pipelines are unaffected. - Selecting by content id resolves through an index built once per invocation instead of a scan
per id: on a 20 000 sub-hunk diff, selecting every id went from 0.22 s to 0.06 s. - Release archives are now verified by running the packaged binary and comparing its
--versionwith the archive name, so a packaging fault is caught before upload. CI gained a
cargo semver-checksgate over the public API, the scheduled fuzz run bounds a single input
with-timeout, and the versions of the tools CI installs are pinned.
v0.8.0
Fixed
- 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, andselect '*'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 patchmarker, sogit applyrejected 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
@Lslice 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. Seedocs/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 — whereemitalready 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
diffgit applyrejects 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-patchappends, a strayBinary 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. splitreplaces 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 applyrejected 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 thediff --gitline, and*takes a hunkless entry whole. - Paths quoted and C-escaped by git (
"a/\303\251.txt", thecore.quotePathdefault 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::MAXfrom 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 inu64, 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 aslistnumbers it. - A reader that closes the pipe first (
hunkpick list | head) ended the run with exit 74
and aBroken pipediagnostic; it is now a normal end of work (exit 0). Both this and the
exit-code change above are recorded indocs/ADR/0013-exit-code-contract.md. - In the
path:setselector form a broken set was re-read together with the path, so
f:2-1was reported asnot a number: f:2instead ofreversed range. splitnow recomputes new-side anchors likeselectdoes, 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 andgit applycalled 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.diffwrites 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
gitthat could not be started for--verify-result-diff-gitwas 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 --jsondid 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 runslistwent from
3.9 s to 0.06 s. splitno longer clones the whole parsed diff to rewrite one hunk.- The
git apply --checkchild process no longer inheritsGIT_DIR,GIT_WORK_TREEand
related variables, so-C DIRalone selects the repository. --coloris described inlist --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.mdwith the license texts of the crates
linked into the binary, and the release pipeline builds and verifies every archive
before the irreversiblecargo publishrather than after it. The release procedure is
written down inRELEASING.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. Seedocs/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-gitdocuments 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 fromCargo.tomland publishes nothing. - Behavioural decisions of this release are recorded as ADRs:
docs/ADR/0012-two-sided-diffs-only.md
anddocs/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.rscompares 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.rsusesproptestfor the forms git
will not produce on demand (CRLF, a missing final newline, a mail preamble), andfuzz/
holds libFuzzer targets for parsing, theparse . emitfixed point and selector handling,
with committed seeds (`fuzz/seeds...
v0.7.0
Fixed
selectnow recomputes the new-side (+) start of every emitted hunk instead of
carrying over the value from the input diff. Leaving a sub-hunk out changes how many
lines the result adds or removes above each later hunk, so the inherited anchors
described a file the selection does not produce.git applystarts its search at the
new-side position: with a drifted anchor a selection could be rejected
(patch does not apply) or, where the surrounding context occurs more than once,
applied cleanly to the wrong occurrence. Selecting several sub-hunks in one invocation
no longer needs thediff → stage → re-diffloop as a workaround.
Changed
- The default internal consistency check also verifies the new-side starts against the
accumulatedadded - deletedof the preceding hunks, reported as aStaleNewStart
error. A diff whose anchors are stale (for example, a result diff produced by an
earlier version) is now rejected rather than passed on togit apply.
v0.6.0
Added
CLICOLOR_FORCE(any non-empty value) forces coloured output in the default
--color automode even when stdout is not a terminal (e.g. a pipe).
NO_COLORstill takes precedence when both are set, and an explicit
--color always|neveroverrides both.
Changed
- Result-diff validation now rejects a change-free (all-context) hunk with a
dedicatedNoChangeHunkerror. Such a hunk balances the header counts but
git applyrejects it; it is unreachable from a real git diff and only
possible from a synthetic patch. - The parser no longer appends body lines past the counts declared in a hunk
header: once a side's declared count is exhausted, a further line of that kind
ends the hunk instead of being absorbed into it. Well-formed git diffs are
unaffected.
v0.5.1
Fixed
parseno longer mistakes a deletion line whose content begins with--
for a new-file header. Such a deletion renders as--- <text>in the hunk
body; new-file detection previously triggered on any---line while inside
a hunk, dropping the real change and emitting a phantom file. The parser now
tracks the old/new line counts declared by the hunk header and only treats
---as the next file once the current hunk body is fully consumed.read_limitedno longer overflowslimit + 1at--max-input-bytes
18446744073709551615(u64::MAX), which wrapped to0in release builds
and silently treated any input as empty. It now uses a saturating add.split --aton a hunk's first context line no longer emits a leading
context-only piece with zero net change (a hunkgit applyrejects); such a
piece is dropped.
v0.5.0
Removed
- Breaking: the
[path:]INDEX@lo-hiadded-line range selector (added in
0.4.0) is removed. Its contiguous-slice implementation produced an
unapplicable hunk for a leading or interior slice of an addition block placed
mid-file: it kept the leading context but dropped the trailing context, so
git applycould not anchor the piece (patch does not apply). The per-line
[path:]INDEX@L<set>selector supersedes it:@Laddresses any subset of a
sub-hunk's changed lines, keeps both leading and trailing context (every
subset applies), and can express any added-line range plus what@lo-hi
could not (deletions, interior slices, replacements). A selector that still
uses the@lo-hiform now fails with exit 2 and a message pointing at the
@Lreplacement.
Changed
- The human
listmarker for an all-additions sub-hunk is renamed from
[+range]to[+add](the@lo-hirange form it advertised is gone). The
addition_onlyfield inlist --jsonis unchanged.
v0.4.0
Added
selectaccepts a per-line changed-line selector[path:]INDEX@L<set>that
keeps an arbitrary subset of a sub-hunk's changed (+/-) lines, where
<set>is an index list over the changed lines (L1,3,L1-2,4). Unlike
INDEX@RANGE(added-side only, cut between two+lines, deletions atomic),
it has no boundary restriction: a deletion surrounded by additions
(+x -y +z) can be isolated, and the deletions and additions of a
replacement can be separated across commits via the diff → stage → re-diff
loop. Unselected deletions are kept as context (anchoring the hunk, so the
old-side footprint is invariant and no--unidiff-zerois needed);
unselected additions are dropped. A sub-hunk addressed by@Lmust be
addressed once per invocation (exit 2 otherwise).list --jsonreports achanged_linesarray per sub-hunk —
[{i, kind, text}]with 1-based indices shared across deletions and
additions — the machine-readable source for building@Lselectors without
parsing the diff body.
Tests
- Added unit and end-to-end coverage for the line-set selector: separating
deletions from additions, isolating a deletion among additions, full-selection
round-trip, the no-newline-at-EOF re-add edge, the once-per-invocation rule,
and thechanged_linesJSON numbering.
v0.3.1
Fixed
selectnow orders multipleINDEX@RANGEselectors that address the same
sub-hunk by their first added line, so two disjoint ranges given in any order
(e.g.1@3-4 1@1-2) emit in ascending new-file order and apply cleanly
instead of being rejected as overlapping.- Overlapping added-line ranges of one sub-hunk (e.g.
1@1-3 1@2-4) are now
rejected as a selector error (exit 2) before emission, so they can no longer
slip past--no-verify-result-diff-internalas a corrupt diff. - A malformed selector now reports the specific reason it was rejected — a
reversed range, a zero bound, or a non-numeric bound — instead of a bare
bad selector: <text>.
Tests
- Added unit and end-to-end coverage for range ordering, overlap rejection
(including under--no-verify-result-diff-internal), and the specific
bad-selector reasons.