Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
35 changes: 35 additions & 0 deletions internal/repo/compact_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -777,3 +777,38 @@ func TestAPureDeletionIsNeverTruncated(t *testing.T) {
t.Fatalf("the tail was dropped: %q", got)
}
}

func TestCompactKeepsAFilesCRLFLineEndings(t *testing.T) {
// A Windows checkout with core.autocrlf (Git for Windows' default)
// has CRLF on every line. The patch is written with plain newlines,
// and matching already tolerates that; what it wrote back did not,
// leaving every line the hunk touched with LF in an otherwise CRLF
// file.
repository := project(t, map[string]string{
"app.py": "import os\r\n\r\ndef start():\r\n server.run(config)\r\n return server\r\n",
})
err := repository.Apply("@app.py\n def start():\n- server.run(config)\n+ server.run(config, debug=True)\n return server\n")
if err != nil {
t.Fatal(err)
}
want := "import os\r\n\r\ndef start():\r\n server.run(config, debug=True)\r\n return server\r\n"
if got := read(t, repository, "app.py"); got != want {
t.Fatalf("line endings not kept:\n got %q\nwant %q", got, want)
}
}

func TestApplyPatchFormatKeepsAFilesCRLFLineEndings(t *testing.T) {
// The same kind of file edited through the apply_patch format, whose
// lines may carry the CR themselves when the model copied them from
// the file byte for byte.
repository := project(t, map[string]string{
"a.txt": "one\r\ntwo\r\nthree\r\n",
})
err := repository.Apply("*** Begin Patch\n*** Update File: a.txt\n@@\n one\r\n-two\r\n+TWO\r\n three\n*** End Patch\n")
if err != nil {
t.Fatal(err)
}
if got, want := read(t, repository, "a.txt"), "one\r\nTWO\r\nthree\r\n"; got != want {
t.Fatalf("line endings not kept:\n got %q\nwant %q", got, want)
}
}
32 changes: 31 additions & 1 deletion internal/repo/patch.go
Original file line number Diff line number Diff line change
Expand Up @@ -422,7 +422,7 @@ func (repository *Repository) applyByContent(patch string) error {
current = read
originals[file.path] = read
}
content, hunkFailures := applyHunks(current, file.hunks)
content, hunkFailures := applyKeepingLineEndings(current, file.hunks)
for _, failure := range hunkFailures {
// The file is known here and not where the failure was
// raised, so it is filled in on the way past.
Expand Down Expand Up @@ -483,6 +483,36 @@ func changesNothing(current hunk) bool {
return true
}

// applyKeepingLineEndings is applyHunks for a file that may use CRLF.
// Hunks are written with plain newlines, so in a CRLF file every line
// only matched through locate's whitespace-insensitive pass, and the
// lines put back in its place came from the hunk without their CR: each
// edit left the lines it touched with LF inside an otherwise CRLF file.
// A file whose every line ends in CRLF is edited as LF and converted
// back; anything else (LF, mixed, a new file) is applied as it stands.
func applyKeepingLineEndings(content string, hunks []hunk) (string, []*HunkError) {
breaks := strings.Count(content, "\n")
if breaks == 0 || strings.Count(content, "\r\n") != breaks {
return applyHunks(content, hunks)
}
withoutCR := func(lines []string) []string {
trimmed := make([]string, len(lines))
for index, line := range lines {
trimmed[index] = strings.TrimSuffix(line, "\r")
}
return trimmed
}
trimmed := make([]hunk, len(hunks))
for index, current := range hunks {
trimmed[index] = hunk{before: withoutCR(current.before), after: withoutCR(current.after)}
for _, op := range current.ops {
trimmed[index].ops = append(trimmed[index].ops, patchOp{kind: op.kind, text: strings.TrimSuffix(op.text, "\r")})
}
}
placed, failures := applyHunks(strings.ReplaceAll(content, "\r\n", "\n"), trimmed)
return strings.ReplaceAll(placed, "\n", "\r\n"), failures
}

func applyHunks(content string, hunks []hunk) (string, []*HunkError) {
lines := strings.Split(content, "\n")
var failures []*HunkError
Expand Down