diff --git a/packages/core/src/patch.ts b/packages/core/src/patch.ts index a297761fe0..81877e0d78 100644 --- a/packages/core/src/patch.ts +++ b/packages/core/src/patch.ts @@ -157,6 +157,10 @@ function parseUpdate(lines: ReadonlyArray, start: number, end: number) { else if (line.startsWith("+")) newLines.push(line.slice(1)) index++ } + if (lines[index]?.trim() === "*** End of File") { + endOfFile = true + index++ + } chunks.push({ oldLines, newLines, changeContext, endOfFile: endOfFile || undefined }) } return { chunks, next: index } @@ -192,11 +196,15 @@ function computeReplacements(lines: ReadonlyArray, path: string, chunks: function seek(lines: ReadonlyArray, pattern: ReadonlyArray, start: number, eof = false) { if (pattern.length === 0) return -1 - for (const compare of [exact, rstrip, trim, normalized]) { - if (eof) { - const offset = lines.length - pattern.length - if (offset >= start && matches(lines, pattern, offset, compare)) return offset + if (eof) { + const offset = lines.length - pattern.length + if (offset < start) return -1 + for (const compare of [exact, rstrip, trim, normalized]) { + if (matches(lines, pattern, offset, compare)) return offset } + return -1 + } + for (const compare of [exact, rstrip, trim, normalized]) { for (let offset = start; offset <= lines.length - pattern.length; offset++) { if (matches(lines, pattern, offset, compare)) return offset } diff --git a/packages/core/test/patch.test.ts b/packages/core/test/patch.test.ts index 9646452f17..f5bdddb333 100644 --- a/packages/core/test/patch.test.ts +++ b/packages/core/test/patch.test.ts @@ -113,6 +113,21 @@ describe("Patch", () => { ]) }) + test("preserves the end-of-file marker", () => { + expect( + parse( + "*** Begin Patch\n*** Update File: file.txt\n@@\n+quux\n*** End of File\n\n*** End Patch", + ), + ).toEqual([ + { + type: "update", + path: "file.txt", + movePath: undefined, + chunks: [{ oldLines: [], newLines: ["quux"], changeContext: undefined, endOfFile: true }], + }, + ]) + }) + test("derives fuzzy line updates while preserving BOM", () => { const update = Patch.derive("update.txt", [{ oldLines: [" old "], newLines: ["new"] }], "\uFEFFold\n") expect(update).toEqual({ content: "new\n", bom: true }) @@ -174,6 +189,16 @@ describe("Patch", () => { ).toBe("marker\nmiddle\nmarker changed\nend\n") }) + test("does not fall back to a non-EOF match", () => { + expect(() => + Patch.derive( + "update.txt", + [{ oldLines: ["marker", "end"], newLines: ["changed", "end"], endOfFile: true }], + "marker\nend\nmiddle\n", + ), + ).toThrow("Failed to find expected lines") + }) + test("matches V1 lenient parsing of malformed hunk bodies", () => { expect(parse("*** Begin Patch\n*** Add File: add.txt\nmissing plus\n*** End Patch")).toEqual([ { type: "add", path: "add.txt", contents: "" }, diff --git a/packages/core/test/tool-patch.test.ts b/packages/core/test/tool-patch.test.ts index 05cee56418..8395335b92 100644 --- a/packages/core/test/tool-patch.test.ts +++ b/packages/core/test/tool-patch.test.ts @@ -392,14 +392,32 @@ describe("PatchTool", () => { withTempTool((directory, registry) => Effect.gen(function* () { const target = path.join(directory, "tail.txt") - yield* Effect.promise(() => fs.writeFile(target, "alpha\nlast\n")) + yield* Effect.promise(() => fs.writeFile(target, "first\nsecond")) yield* executeTool( registry, call( - "*** Begin Patch\n*** Update File: tail.txt\n@@\n-last\n+end\n*** End of File\n*** End Patch", + "*** Begin Patch\n*** Update File: tail.txt\n@@\n first\n-second\n+second updated\n*** End of File\n*** End Patch", ), ) - expect(yield* Effect.promise(() => fs.readFile(target, "utf8"))).toBe("alpha\nend\n") + expect(yield* Effect.promise(() => fs.readFile(target, "utf8"))).toBe("first\nsecond updated\n") + }), + ), + ) + + it.live("applies an end-of-file chunk to the final duplicate", () => + withTempTool((directory, registry) => + Effect.gen(function* () { + const target = path.join(directory, "duplicates.txt") + yield* Effect.promise(() => fs.writeFile(target, "marker\nend\nmiddle\nmarker\nend\n")) + yield* executeTool( + registry, + call( + "*** Begin Patch\n*** Update File: duplicates.txt\n@@\n-marker\n-end\n+marker changed\n+end\n*** End of File\n*** End Patch", + ), + ) + expect(yield* Effect.promise(() => fs.readFile(target, "utf8"))).toBe( + "marker\nend\nmiddle\nmarker changed\nend\n", + ) }), ), )