Skip to content

patch_set: accept CRLF line endings in all patch syntax - #90

Merged
bmwill merged 3 commits into
masterfrom
crlf-line-endings
Oct 8, 2026
Merged

bmwill merged 3 commits into
masterfrom
crlf-line-endings

Conversation

@bmwill

@bmwill bmwill commented Oct 8, 2026 •

Copy link
Copy Markdown
Owner

Using #89 as inspiration to do a bit more tightening around CRLF handling.

Brandon Williams added 3 commits October 8, 2026 12:05
Previously, only the `---`/`+++` headers accepted CRLF line endings (#87).
The git extended headers kept the `\r` in their values, so in a CRLF patch
mode lines failed with `InvalidFileMode("100644\r")`, renames and copies
failed with `InvalidCharInUnquotedFilename`, and mode-only changes failed
with `InvalidDiffGitPath`. A `git format-patch` email with CRLF line
endings also kept its commit message, because the separator was matched
as `\n---\n`, so a message line starting with `diff --git` began a bogus
patch. The same bug was reported in #89.

With this commit, `Text::strip_line_ending` strips either `\n` or `\r\n`,
and every parser that reads patch syntax uses it: the git extended
headers, the `---`/`+++` filenames, the binary patch parser (which had
two hand-written copies), and the email separator. A `\r` that no `\n`
follows is not a line ending, as in #87.

Hunk lines are unchanged: they keep their `\r` as content, which matches
`git apply`, and the docs for `Line` and `PatchSet` now say so. GNU patch
instead strips the `\r` from every line of a CRLF patch.

Also make the gitdiff test helper check that the CRLF form of each input
parses the same as the LF form, and add tests for format-patch emails,
hunk content, the bytes API, and a lone `\r`.
snapbox treats `\r\n` and `\n` as equal when it compares text files, so
the compat harness could not tell whether a patched line kept its `\r`.

Add an opt-in `Case::exact_line_endings` that also compares the output
files byte for byte, against both the `out/` snapshot and the external
tool's output. The `out/` files of these cases are written by hand,
because `SNAPSHOTS=overwrite` normalizes their line endings, and
`.gitattributes` marks the `crlf_*` fixtures `-text` so that a checkout
with `core.autocrlf` cannot convert them.

Add CRLF cases against `git apply` (create, modify, rename with a mode
change, and mode-only change) and GNU patch (a CRLF patch on an LF file
and on a CRLF file). They record where diffy diverges on purpose:

* `git apply` rejects a CRLF mode-only change ("git diff header lacks
  filename information"); diffy accepts it, like GNU patch.
* GNU patch strips the `\r` from every line of a CRLF patch, so it
  patches LF files and rejects CRLF ones. diffy keeps the `\r`, like
  `git apply`, so the results are the opposite.

The expected outputs were verified against git 2.55.0 and GNU patch 2.8.
Previously, the formatter wrote two spaces before the function context
that `diff -p` and `git diff` add after the closing `@@` of a hunk header,
and then a newline. The parsed function context already ends with the
header's line ending, so formatting `@@ -4,6 +4,6 @@ int main(void)`
produced `@@ -4,6 +4,6 @@  int main(void)` followed by a blank line. Both
GNU patch and `git apply` reject that patch, because the blank line counts
as an extra context line, and each round trip added another leading space.

With this commit, the formatter writes a single space before the function
context, matching GNU diff 3.12 and git 2.55, and adds a newline only when
the function context doesn't already end in one. Formatting a parsed patch
now reproduces its hunk headers byte for byte, including a `\r\n` ending.
`Hunk::function_context` still includes the line ending, and its docs now
say so.

Also add tests for the round trip on both the str and bytes paths, a CRLF
hunk header, absent and empty function contexts, a hunk header at the end
of the input, and colored output.
@bmwill
bmwill merged commit eba5e53 into master Oct 8, 2026
46 checks passed
@bmwill
bmwill deleted the crlf-line-endings branch October 8, 2026 19:10
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