fix(core): compose successive patch updates (#38034)

This commit is contained in:
Aiden Cline 2026-07-20 23:36:48 -05:00 committed by GitHub
commit eb4ff91c2d
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
2 changed files with 46 additions and 22 deletions

View file

@ -111,6 +111,7 @@ export const Plugin = {
} }
const prepared: Prepared[] = [] const prepared: Prepared[] = []
const targets: Target[] = [] const targets: Target[] = []
const updates = new Map<string, string>()
for (const hunk of hunks) { for (const hunk of hunks) {
yield* Effect.gen(function* () { yield* Effect.gen(function* () {
const target = resolveTarget(location, hunk.path) const target = resolveTarget(location, hunk.path)
@ -154,28 +155,34 @@ export const Plugin = {
prepared.push({ ...hunk, target, before: original.replace(/^\uFEFF/, ""), after: "" }) prepared.push({ ...hunk, target, before: original.replace(/^\uFEFF/, ""), after: "" })
return return
} }
const stats = yield* fs.stat(target.canonical).pipe( const previous = updates.get(target.canonical)
Effect.mapError( const original =
(error) => previous ??
new ToolFailure({ (yield* Effect.gen(function* () {
message: `patch verification failed: Failed to read file to update ${target.canonical}: ${error instanceof Error ? error.message : String(error)}`, const stats = yield* fs.stat(target.canonical).pipe(
}), Effect.mapError(
), (error) =>
) new ToolFailure({
if (stats.type === "Directory") { message: `patch verification failed: Failed to read file to update ${target.canonical}: ${error instanceof Error ? error.message : String(error)}`,
return yield* new ToolFailure({ }),
message: `patch verification failed: Failed to read file to update ${target.canonical}: path is a directory`, ),
}) )
} if (stats.type === "Directory") {
const content = yield* fs.readFile(target.canonical).pipe( return yield* new ToolFailure({
Effect.mapError( message: `patch verification failed: Failed to read file to update ${target.canonical}: path is a directory`,
(error) => })
new ToolFailure({ }
message: `patch verification failed: Failed to read file to update ${target.canonical}: ${error instanceof Error ? error.message : String(error)}`, return new TextDecoder("utf-8", { ignoreBOM: true }).decode(
}), yield* fs.readFile(target.canonical).pipe(
), Effect.mapError(
) (error) =>
const original = new TextDecoder("utf-8", { ignoreBOM: true }).decode(content) new ToolFailure({
message: `patch verification failed: Failed to read file to update ${target.canonical}: ${error instanceof Error ? error.message : String(error)}`,
}),
),
),
)
}))
const before = original.replace(/^\uFEFF/, "") const before = original.replace(/^\uFEFF/, "")
const update = yield* Effect.try({ const update = yield* Effect.try({
try: () => Patch.derive(hunk.path, hunk.chunks, original), try: () => Patch.derive(hunk.path, hunk.chunks, original),
@ -205,6 +212,7 @@ export const Plugin = {
after: update.content, after: update.content,
moveTarget, moveTarget,
}) })
if (!moveTarget) updates.set(target.canonical, Patch.joinBom(update.content, update.bom))
}).pipe(Effect.mapError((error) => (error instanceof ToolFailure ? error : fail(hunk.path, error)))) }).pipe(Effect.mapError((error) => (error instanceof ToolFailure ? error : fail(hunk.path, error))))
} }

View file

@ -487,6 +487,22 @@ describe("PatchTool", () => {
), ),
) )
it.live("applies successive update operations to one file", () =>
withTempTool((directory, registry) =>
Effect.gen(function* () {
const target = path.join(directory, "successive.txt")
yield* Effect.promise(() => fs.writeFile(target, "a\nb\n"))
yield* executeTool(
registry,
call(
"*** Begin Patch\n*** Update File: successive.txt\n@@\n-a\n+A\n*** Update File: successive.txt\n@@\n-b\n+B\n*** End Patch",
),
)
expect(yield* Effect.promise(() => fs.readFile(target, "utf8"))).toBe("A\nB\n")
}),
),
)
it.live("does not invent a first-line diff for BOM files", () => it.live("does not invent a first-line diff for BOM files", () =>
withTempTool((directory, registry) => withTempTool((directory, registry) =>
Effect.gen(function* () { Effect.gen(function* () {