From fdb97ea52098de4274398a4aa37b741fdbc73348 Mon Sep 17 00:00:00 2001 From: Alexander Poss Date: Sat, 14 Feb 2026 12:17:13 +0100 Subject: [PATCH 1/2] fix(patch): handle Windows drive-letter colons in patch header path parsing parsePatchHeader used line.split(':', 2) to extract file paths from headers like '*** Update File: D:\path\file.ts'. On Windows, the colon in the drive letter (D:) caused split to truncate the path to just the drive letter, producing invalid paths like 'D:\project\D'. Replace split(':') with slice() after the known prefix length so the full path including drive letter is preserved. --- packages/opencode/src/patch/index.ts | 8 +++--- packages/opencode/test/patch/patch.test.ts | 31 ++++++++++++++++++++++ 2 files changed, 35 insertions(+), 4 deletions(-) diff --git a/packages/opencode/src/patch/index.ts b/packages/opencode/src/patch/index.ts index 0efeff544f66..b87ad5552865 100644 --- a/packages/opencode/src/patch/index.ts +++ b/packages/opencode/src/patch/index.ts @@ -79,23 +79,23 @@ export namespace Patch { const line = lines[startIdx] if (line.startsWith("*** Add File:")) { - const filePath = line.split(":", 2)[1]?.trim() + const filePath = line.slice("*** Add File:".length).trim() return filePath ? { filePath, nextIdx: startIdx + 1 } : null } if (line.startsWith("*** Delete File:")) { - const filePath = line.split(":", 2)[1]?.trim() + const filePath = line.slice("*** Delete File:".length).trim() return filePath ? { filePath, nextIdx: startIdx + 1 } : null } if (line.startsWith("*** Update File:")) { - const filePath = line.split(":", 2)[1]?.trim() + const filePath = line.slice("*** Update File:".length).trim() let movePath: string | undefined let nextIdx = startIdx + 1 // Check for move directive if (nextIdx < lines.length && lines[nextIdx].startsWith("*** Move to:")) { - movePath = lines[nextIdx].split(":", 2)[1]?.trim() + movePath = lines[nextIdx].slice("*** Move to:".length).trim() nextIdx++ } diff --git a/packages/opencode/test/patch/patch.test.ts b/packages/opencode/test/patch/patch.test.ts index 020253bfe2d1..04d40f4894a8 100644 --- a/packages/opencode/test/patch/patch.test.ts +++ b/packages/opencode/test/patch/patch.test.ts @@ -80,6 +80,37 @@ describe("Patch namespace", () => { } }) + test("should parse paths containing colons (Windows drive letters)", () => { + const patchText = `*** Begin Patch +*** Add File: C:\\Users\\user\\project\\new.txt ++content +*** Delete File: D:\\repos\\project\\old.txt +*** Update File: E:\\work\\src\\main.ts +*** Move to: E:\\work\\src\\renamed.ts +@@ +-old ++new +*** End Patch` + + const result = Patch.parsePatch(patchText) + expect(result.hunks).toHaveLength(3) + + const add = result.hunks[0] + expect(add.type).toBe("add") + expect(add.path).toBe("C:\\Users\\user\\project\\new.txt") + + const del = result.hunks[1] + expect(del.type).toBe("delete") + expect(del.path).toBe("D:\\repos\\project\\old.txt") + + const update = result.hunks[2] + expect(update.type).toBe("update") + expect(update.path).toBe("E:\\work\\src\\main.ts") + if (update.type === "update") { + expect(update.move_path).toBe("E:\\work\\src\\renamed.ts") + } + }) + test("should throw error for invalid patch format", () => { const invalidPatch = `This is not a valid patch` From 8b197f9e11d2d0b9ddfb1d2763fbecd29324dc59 Mon Sep 17 00:00:00 2001 From: Alexander Poss Date: Sat, 14 Feb 2026 12:25:25 +0100 Subject: [PATCH 2/2] refactor: extract patch header prefix strings into constants --- packages/opencode/src/patch/index.ts | 21 +++++++++++++-------- 1 file changed, 13 insertions(+), 8 deletions(-) diff --git a/packages/opencode/src/patch/index.ts b/packages/opencode/src/patch/index.ts index b87ad5552865..f1952f79a1af 100644 --- a/packages/opencode/src/patch/index.ts +++ b/packages/opencode/src/patch/index.ts @@ -72,30 +72,35 @@ export namespace Patch { } // Parser implementation + const ADD_PREFIX = "*** Add File:" + const DELETE_PREFIX = "*** Delete File:" + const UPDATE_PREFIX = "*** Update File:" + const MOVE_PREFIX = "*** Move to:" + function parsePatchHeader( lines: string[], startIdx: number, ): { filePath: string; movePath?: string; nextIdx: number } | null { const line = lines[startIdx] - if (line.startsWith("*** Add File:")) { - const filePath = line.slice("*** Add File:".length).trim() + if (line.startsWith(ADD_PREFIX)) { + const filePath = line.slice(ADD_PREFIX.length).trim() return filePath ? { filePath, nextIdx: startIdx + 1 } : null } - if (line.startsWith("*** Delete File:")) { - const filePath = line.slice("*** Delete File:".length).trim() + if (line.startsWith(DELETE_PREFIX)) { + const filePath = line.slice(DELETE_PREFIX.length).trim() return filePath ? { filePath, nextIdx: startIdx + 1 } : null } - if (line.startsWith("*** Update File:")) { - const filePath = line.slice("*** Update File:".length).trim() + if (line.startsWith(UPDATE_PREFIX)) { + const filePath = line.slice(UPDATE_PREFIX.length).trim() let movePath: string | undefined let nextIdx = startIdx + 1 // Check for move directive - if (nextIdx < lines.length && lines[nextIdx].startsWith("*** Move to:")) { - movePath = lines[nextIdx].slice("*** Move to:".length).trim() + if (nextIdx < lines.length && lines[nextIdx].startsWith(MOVE_PREFIX)) { + movePath = lines[nextIdx].slice(MOVE_PREFIX.length).trim() nextIdx++ }