Skip to content

Fix applying patches with zero-context fragments#81

Open
gaoflow wants to merge 1 commit into
bluekeyes:masterfrom
gaoflow:fix-zero-context-apply
Open

Fix applying patches with zero-context fragments#81
gaoflow wants to merge 1 commit into
bluekeyes:masterfrom
gaoflow:fix-zero-context-apply

Conversation

@gaoflow

@gaoflow gaoflow commented Jul 26, 2026

Copy link
Copy Markdown

Zero-context (-U0) insertions are applied one line too early, and Apply returns nil, so the output is silently wrong rather than an error:

src: A B C     patch: @@ -3,0 +4 @@
                      +D

go-gitdiff:               A B D C
git apply --unidiff-zero: A B C D
GNU patch:                A B C D

A zero-length range in a hunk header is positional — @@ -3,0 +4 @@ inserts after old line 3 — but the applier starts every fragment at OldPosition - 1. Two nearby checks read file-level facts off the same ambiguous positions and reject patches that git apply --unidiff-zero and GNU patch both accept: @@ -0,0 +1 @@ (insert before line 1) is read as file creation, and @@ -1 +0,0 @@ (delete line 1) as a full delete. The fix starts a fragment at OldPosition when it has no old lines, and moves those two checks into Apply behind File.IsNew/File.IsDelete — the fields ParseTextFragments already uses to answer the same questions.

Tested by diffing against git apply --unidiff-zero and GNU patch over ~9,700 generated patches (-U0-U3, insert/delete/replace at file start, middle and end, multi-hunk, empty files, no trailing newline, CRLF, and the File.String() round-trip): 162 wrong results at -U0 before, 0 after, -U1-U3 unchanged. One existing expectation changes — text_fragment_error_new_file.patch is not a new-file patch (no /dev/null side, no new file mode), and git and GNU patch both apply it as an insertion before line 1, so it is replaced by a file-level test using a real new-file patch that still reports a conflict.

In unified diff format a range with a length of zero is positional: the
hunk "@@ -3,0 +4 @@" inserts after old line 3 rather than starting at it.
The text applier derived the start of every fragment from OldPosition the
same way, so each zero-context insertion was applied one line too early.
Apply returned nil in that case, so the result was silently corrupted
rather than reported as an error.

Two nearby checks read file-level facts off the same ambiguous positions.
An OldPosition of 0 was taken to mean "create a file", but "@@ -0,0 +1 @@"
is also how a patch inserts before the first line of an existing file. A
new position of "+0,0" was taken to mean "delete the file", but
"@@ -1 +0,0 @@" is also how a patch deletes only the first line. Both
rejected patches that git apply --unidiff-zero and GNU patch accept.

Start a fragment at OldPosition when it has no old lines, and move the
creation and deletion checks to Apply, where File.IsNew and File.IsDelete
are available. This matches ParseTextFragments, which already uses those
fields rather than fragment positions to decide the same questions.

text_fragment_error_new_file.patch is not a new file patch: it has no
/dev/null side and no new file mode line, so git and GNU patch both apply
it as an insertion before line 1. Replace it with a file-level test that
uses a real new file patch, which still reports a conflict.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant