From 13fd274a76f4e4391b7170ed76cd959ce8886277 Mon Sep 17 00:00:00 2001 From: Wout Stiens <71498452+StiensWout@users.noreply.github.com> Date: Sat, 14 Nov 2026 16:56:42 +0100 Subject: [PATCH 1/5] fix(server): Commit refuses UTF-16 files that still have conflict markers Git numbers its leftover-marker reports in its decoded view of the file, but the guard reread the raw bytes as UTF-8, so a UTF-16 file never matched and commit-all staged the unresolved conflict. Read the reported lines through Git instead, by diffing the file against the empty tree. Co-authored-by: Claude Opus 5.5 Co-authored-by: GPT 6.1 Sol --- apps/server/src/vcs/GitVcsDriverCore.test.ts | 47 ++++++++++++----- apps/server/src/vcs/GitVcsDriverCore.ts | 34 +++++++++--- .../utf16-merge-conflict-blocks-commit.e2e.ts | 52 +++++++++++++++++++ 3 files changed, 114 insertions(+), 19 deletions(-) create mode 100644 scripts/scenarios/utf16-merge-conflict-blocks-commit.e2e.ts diff --git a/apps/server/src/vcs/GitVcsDriverCore.test.ts b/apps/server/src/vcs/GitVcsDriverCore.test.ts index 76b1fb4f898b5..313525005c347 100644 --- a/apps/server/src/vcs/GitVcsDriverCore.test.ts +++ b/apps/server/src/vcs/GitVcsDriverCore.test.ts @@ -7396,30 +7396,49 @@ it.layer(layerTest)("GitVcsDriver core integration", (it) => { ); it.effect.each([ - { location: "root", conflict: false }, - { location: "nested", conflict: false }, - { location: "worktree", conflict: false }, - { location: "root", conflict: true }, - { location: "nested", conflict: true }, - { location: "worktree", conflict: true }, + { location: "root", conflict: false, encoding: "utf8" }, + { location: "nested", conflict: false, encoding: "utf8" }, + { location: "worktree", conflict: false, encoding: "utf8" }, + { location: "root", conflict: true, encoding: "utf8" }, + { location: "nested", conflict: true, encoding: "utf8" }, + { location: "worktree", conflict: true, encoding: "utf8" }, + { location: "root", conflict: false, encoding: "utf16le" }, + { location: "root", conflict: true, encoding: "utf16le" }, ])( - "rejects selected-file merge commits without mutation ($location, conflict: $conflict)", - ({ location, conflict }) => + "rejects selected-file merge commits without mutation ($location, conflict: $conflict, $encoding)", + ({ location, conflict, encoding }) => Effect.gen(function* () { let cwd = yield* makeTmpDir(); const { initialBranch } = yield* initRepoWithCommit(cwd); + const writeReadme = (text: string) => + Effect.sync(() => + NodeFS.writeFileSync( + NodePath.join(cwd, "README.md"), + encoding === "utf16le" ? Buffer.from(`\ufeff${text}`, "utf16le") : text, + ), + ); + if (encoding === "utf16le") { + yield* writeTextFile( + cwd, + ".gitattributes", + "README.md text working-tree-encoding=UTF-16\n", + ); + yield* writeReadme("base\n"); + yield* git(cwd, ["add", "."]); + yield* git(cwd, ["commit", "-m", "Declare UTF-16 encoding"]); + } const driver = yield* GitVcsDriver.GitVcsDriver; const fs = yield* FileSystem.FileSystem; const path = yield* Path.Path; yield* git(cwd, ["checkout", "-b", "feature"]); yield* writeTextFile(cwd, "feature.txt", "feature\n"); - if (conflict) yield* writeTextFile(cwd, "README.md", "feature version\n"); + if (conflict) yield* writeReadme("feature version\n"); yield* git(cwd, ["add", "."]); yield* git(cwd, ["commit", "-m", "Feature"]); const featureHead = yield* git(cwd, ["rev-parse", "HEAD"]); yield* git(cwd, ["checkout", initialBranch]); yield* writeTextFile(cwd, "main.txt", "main\n"); - if (conflict) yield* writeTextFile(cwd, "README.md", "main version\n"); + if (conflict) yield* writeReadme("main version\n"); yield* git(cwd, ["add", "."]); yield* git(cwd, ["commit", "-m", "Main"]); const mainHead = yield* git(cwd, ["rev-parse", "HEAD"]); @@ -7472,11 +7491,15 @@ it.layer(layerTest)("GitVcsDriver core integration", (it) => { const unresolved = yield* driver .commit(commitCwd, "Unresolved merge", "", { stage: {} }) .pipe(Effect.result); - assert.isTrue(Result.isFailure(unresolved), "committed an unresolved conflict"); + if (Result.isSuccess(unresolved)) { + assert.fail( + `committed an unresolved conflict: ${yield* git(cwd, ["show", "HEAD:README.md"])}`, + ); + } assert.equal(yield* git(cwd, ["rev-parse", "HEAD"]), mainHead); assert.equal(yield* git(cwd, ["ls-files", "--unmerged"]), unmerged); // A lone "=======" is a heading underline, not a conflict boundary. - yield* writeTextFile(cwd, "README.md", "Heading\n=======\n\nresolved\n"); + yield* writeReadme("Heading\n=======\n\nresolved\n"); assert.isNotNull(yield* driver.prepareCommitContext(commitCwd)); } yield* driver.commit(commitCwd, "Finish merge", "", { stage: {} }); diff --git a/apps/server/src/vcs/GitVcsDriverCore.ts b/apps/server/src/vcs/GitVcsDriverCore.ts index 280664cf8a136..3f75ac2c038c2 100644 --- a/apps/server/src/vcs/GitVcsDriverCore.ts +++ b/apps/server/src/vcs/GitVcsDriverCore.ts @@ -2696,7 +2696,7 @@ export const makeGitVcsDriverCore = Effect.fn("makeGitVcsDriverCore")(function* ); const reports = check.stdout.split("\n"); const conflicted: string[] = []; - let topLevel: string | undefined; + let emptyTree: string | undefined; for (const file of unmergedPaths) { const markerLines = new Set( reports.flatMap((line) => { @@ -2707,11 +2707,26 @@ export const makeGitVcsDriverCore = Effect.fn("makeGitVcsDriverCore")(function* }), ); if (markerLines.size === 0) continue; - topLevel ??= (yield* runGitStdout("GitVcsDriver.commit.topLevel", cwd, [ - "rev-parse", - "--show-toplevel", - ])).trim(); - const lines = (yield* fileSystem.readFileString(path.join(topLevel, file)).pipe( + emptyTree ??= yield* readEmptyTreeHash(cwd); + // Git numbers lines after working-tree-encoding and clean filters, so read them the same + // way: diffing against the empty tree prints every line of Git's view of the file. + const fileDiff = yield* runGitStdoutWithOptions( + "GitVcsDriver.commit.conflictMarkers", + cwd, + [ + "-c", + "diff.relative=false", + "diff", + "--no-color", + "--no-ext-diff", + "--no-textconv", + "--text", + emptyTree, + "--", + `:(top,literal)${file}`, + ], + { ...(env ? { env } : {}), maxOutputBytes: REVIEW_METADATA_MAX_OUTPUT_BYTES }, + ).pipe( Effect.mapError( (cause) => new GitCommandError({ @@ -2722,7 +2737,12 @@ export const makeGitVcsDriverCore = Effect.fn("makeGitVcsDriverCore")(function* cause, }), ), - )).split("\n"); + ); + const diffLines = fileDiff.split("\n"); + const lines = diffLines + .slice(diffLines.findIndex((line) => line.startsWith("@@")) + 1) + .filter((line) => line.startsWith("+")) + .map((line) => line.slice(1)); // Git also reports a lone "=======", which is valid content such as a heading underline. const reported = (marker: string) => lines.some((line, index) => markerLines.has(index) && line.startsWith(marker)); diff --git a/scripts/scenarios/utf16-merge-conflict-blocks-commit.e2e.ts b/scripts/scenarios/utf16-merge-conflict-blocks-commit.e2e.ts new file mode 100644 index 0000000000000..c23ee984d83c4 --- /dev/null +++ b/scripts/scenarios/utf16-merge-conflict-blocks-commit.e2e.ts @@ -0,0 +1,52 @@ +import { expect } from "e2e"; +import * as Effect from "effect/Effect"; +import * as FileSystem from "effect/FileSystem"; + +import { run } from "./shell.ts"; +import { runPromise, test, WEB } from "./t3.ts"; + +const utf16 = (text: string) => Buffer.from(`${text}`, "utf16le"); + +test( + "The commit dialog refuses a merge whose UTF-16 file still has conflict markers", + WEB, + async ({ t3, app, screen }) => { + const project = await t3.project("demo", { + ".gitattributes": "README.md text working-tree-encoding=UTF-16\n", + "README.md": utf16("base\n"), + }); + const git = (args: ReadonlyArray, allowExit: ReadonlyArray = [0]) => + runPromise( + run( + "git", + ["-c", "user.name=Scenario", "-c", "user.email=scenario@example.invalid", ...args], + { cwd: project, allowExit }, + ), + ); + const commitReadme = async (text: string) => { + await runPromise( + Effect.flatMap(FileSystem.FileSystem, (fs) => + fs.writeFile(`${project}/README.md`, utf16(text)), + ), + ); + await git(["commit", "--quiet", "--all", "--message", text.trim()]); + }; + await git(["checkout", "--quiet", "-b", "feature"]); + await commitReadme("feature version\n"); + await git(["checkout", "--quiet", "main"]); + await commitReadme("main version\n"); + await git(["merge", "--quiet", "feature"], [1]); + const thread = await t3.thread({ project, title: "Merge", messages: ["merge"] }); + + await t3.open(thread); + await screen.getByRole("button", "Git action options").tap(); + await screen.getByRole("menuitem", "Commit").tap(); + await screen.getByPlaceholder("Leave empty to auto-generate").fill("Finish merge"); + await screen.getByRole("button", "Commit", { exact: true }).tap(); + // On a failed assertion e2e captures the committed merge itself. + await expect( + screen.getByText("unresolved merge conflicts in README.md", { exact: false }), + ).toBeVisible(); + await app.screenshot("refused"); + }, +); From dfcf0fd6628a217f08f2ae6adf858f0b16f2bc50 Mon Sep 17 00:00:00 2001 From: Wout Stiens <71498452+StiensWout@users.noreply.github.com> Date: Sat, 14 Nov 2026 17:06:12 +0100 Subject: [PATCH 2/5] refactor(server): trim UTF-16 conflict refusal test and comment Co-authored-by: Claude Opus 5.5 Co-authored-by: GPT 6.1 Sol --- apps/server/src/vcs/GitVcsDriverCore.test.ts | 7 +------ apps/server/src/vcs/GitVcsDriverCore.ts | 4 ++-- 2 files changed, 3 insertions(+), 8 deletions(-) diff --git a/apps/server/src/vcs/GitVcsDriverCore.test.ts b/apps/server/src/vcs/GitVcsDriverCore.test.ts index 313525005c347..aa34e57b3e428 100644 --- a/apps/server/src/vcs/GitVcsDriverCore.test.ts +++ b/apps/server/src/vcs/GitVcsDriverCore.test.ts @@ -7402,7 +7402,6 @@ it.layer(layerTest)("GitVcsDriver core integration", (it) => { { location: "root", conflict: true, encoding: "utf8" }, { location: "nested", conflict: true, encoding: "utf8" }, { location: "worktree", conflict: true, encoding: "utf8" }, - { location: "root", conflict: false, encoding: "utf16le" }, { location: "root", conflict: true, encoding: "utf16le" }, ])( "rejects selected-file merge commits without mutation ($location, conflict: $conflict, $encoding)", @@ -7491,11 +7490,7 @@ it.layer(layerTest)("GitVcsDriver core integration", (it) => { const unresolved = yield* driver .commit(commitCwd, "Unresolved merge", "", { stage: {} }) .pipe(Effect.result); - if (Result.isSuccess(unresolved)) { - assert.fail( - `committed an unresolved conflict: ${yield* git(cwd, ["show", "HEAD:README.md"])}`, - ); - } + assert.isTrue(Result.isFailure(unresolved), "committed an unresolved conflict"); assert.equal(yield* git(cwd, ["rev-parse", "HEAD"]), mainHead); assert.equal(yield* git(cwd, ["ls-files", "--unmerged"]), unmerged); // A lone "=======" is a heading underline, not a conflict boundary. diff --git a/apps/server/src/vcs/GitVcsDriverCore.ts b/apps/server/src/vcs/GitVcsDriverCore.ts index 3f75ac2c038c2..936609fe82ae3 100644 --- a/apps/server/src/vcs/GitVcsDriverCore.ts +++ b/apps/server/src/vcs/GitVcsDriverCore.ts @@ -2708,8 +2708,8 @@ export const makeGitVcsDriverCore = Effect.fn("makeGitVcsDriverCore")(function* ); if (markerLines.size === 0) continue; emptyTree ??= yield* readEmptyTreeHash(cwd); - // Git numbers lines after working-tree-encoding and clean filters, so read them the same - // way: diffing against the empty tree prints every line of Git's view of the file. + // --check numbers lines after working-tree-encoding and clean filters; a diff from the + // empty tree prints that same view of the file. const fileDiff = yield* runGitStdoutWithOptions( "GitVcsDriver.commit.conflictMarkers", cwd, From cdbd303869f9807eb6c9dd44bc9625eb59607a08 Mon Sep 17 00:00:00 2001 From: Wout Stiens <71498452+StiensWout@users.noreply.github.com> Date: Sat, 14 Nov 2026 17:11:07 +0100 Subject: [PATCH 3/5] fix(server): allow resolved large encoded merge files Co-authored-by: GPT 6.1 Sol Co-authored-by: Claude Opus 5.5 --- apps/server/src/vcs/GitVcsDriverCore.test.ts | 22 ++++++++++++++----- apps/server/src/vcs/GitVcsDriverCore.ts | 4 ++-- .../utf16-merge-conflict-blocks-commit.e2e.ts | 2 +- 3 files changed, 19 insertions(+), 9 deletions(-) diff --git a/apps/server/src/vcs/GitVcsDriverCore.test.ts b/apps/server/src/vcs/GitVcsDriverCore.test.ts index aa34e57b3e428..6afa75e426b1f 100644 --- a/apps/server/src/vcs/GitVcsDriverCore.test.ts +++ b/apps/server/src/vcs/GitVcsDriverCore.test.ts @@ -7403,9 +7403,11 @@ it.layer(layerTest)("GitVcsDriver core integration", (it) => { { location: "nested", conflict: true, encoding: "utf8" }, { location: "worktree", conflict: true, encoding: "utf8" }, { location: "root", conflict: true, encoding: "utf16le" }, + { location: "root", conflict: true, encoding: "utf16le", markerSize: 10 }, + { location: "root", conflict: true, encoding: "utf16le", large: true }, ])( - "rejects selected-file merge commits without mutation ($location, conflict: $conflict, $encoding)", - ({ location, conflict, encoding }) => + "rejects selected-file merge commits without mutation ($location, conflict: $conflict, $encoding, marker size: $markerSize, large: $large)", + ({ location, conflict, encoding, markerSize, large }) => Effect.gen(function* () { let cwd = yield* makeTmpDir(); const { initialBranch } = yield* initRepoWithCommit(cwd); @@ -7420,7 +7422,7 @@ it.layer(layerTest)("GitVcsDriver core integration", (it) => { yield* writeTextFile( cwd, ".gitattributes", - "README.md text working-tree-encoding=UTF-16\n", + `README.md text working-tree-encoding=UTF-16${markerSize ? ` conflict-marker-size=${markerSize}` : ""}\n`, ); yield* writeReadme("base\n"); yield* git(cwd, ["add", "."]); @@ -7486,6 +7488,7 @@ it.layer(layerTest)("GitVcsDriver core integration", (it) => { assert.equal(yield* git(cwd, ["rev-parse", "HEAD"]), mainHead); assert.equal(yield* git(cwd, ["ls-files", "--unmerged"]), unmerged); + const resolvedText = `Heading\n=======\n\nresolved${large ? "x".repeat(17 * 1024 * 1024) : ""}\n`; if (conflict) { const unresolved = yield* driver .commit(commitCwd, "Unresolved merge", "", { stage: {} }) @@ -7494,8 +7497,10 @@ it.layer(layerTest)("GitVcsDriver core integration", (it) => { assert.equal(yield* git(cwd, ["rev-parse", "HEAD"]), mainHead); assert.equal(yield* git(cwd, ["ls-files", "--unmerged"]), unmerged); // A lone "=======" is a heading underline, not a conflict boundary. - yield* writeReadme("Heading\n=======\n\nresolved\n"); - assert.isNotNull(yield* driver.prepareCommitContext(commitCwd)); + yield* writeReadme(resolvedText); + assert.isNotNull( + yield* driver.prepareCommitContext(commitCwd, undefined, { includePatch: false }), + ); } yield* driver.commit(commitCwd, "Finish merge", "", { stage: {} }); assert.equal( @@ -7505,7 +7510,12 @@ it.layer(layerTest)("GitVcsDriver core integration", (it) => { assert.isFalse(NodeFS.existsSync(path.join(gitDir, "MERGE_HEAD"))); assert.equal(yield* git(cwd, ["show", "HEAD:feature.txt"]), "feature"); assert.equal(yield* git(cwd, ["show", "HEAD:unrelated.txt"]), "unrelated"); - if (conflict) + if (large) { + assert.equal( + yield* git(cwd, ["cat-file", "-s", "HEAD:README.md"]), + String(Buffer.byteLength(resolvedText)), + ); + } else if (conflict) assert.equal( yield* git(cwd, ["show", "HEAD:README.md"]), "Heading\n=======\n\nresolved", diff --git a/apps/server/src/vcs/GitVcsDriverCore.ts b/apps/server/src/vcs/GitVcsDriverCore.ts index 936609fe82ae3..a81198e103e32 100644 --- a/apps/server/src/vcs/GitVcsDriverCore.ts +++ b/apps/server/src/vcs/GitVcsDriverCore.ts @@ -2709,7 +2709,7 @@ export const makeGitVcsDriverCore = Effect.fn("makeGitVcsDriverCore")(function* if (markerLines.size === 0) continue; emptyTree ??= yield* readEmptyTreeHash(cwd); // --check numbers lines after working-tree-encoding and clean filters; a diff from the - // empty tree prints that same view of the file. + // empty tree prints that same view of the file. Read it in full, as the raw read did. const fileDiff = yield* runGitStdoutWithOptions( "GitVcsDriver.commit.conflictMarkers", cwd, @@ -2725,7 +2725,7 @@ export const makeGitVcsDriverCore = Effect.fn("makeGitVcsDriverCore")(function* "--", `:(top,literal)${file}`, ], - { ...(env ? { env } : {}), maxOutputBytes: REVIEW_METADATA_MAX_OUTPUT_BYTES }, + { ...(env ? { env } : {}), maxOutputBytes: Number.POSITIVE_INFINITY }, ).pipe( Effect.mapError( (cause) => diff --git a/scripts/scenarios/utf16-merge-conflict-blocks-commit.e2e.ts b/scripts/scenarios/utf16-merge-conflict-blocks-commit.e2e.ts index c23ee984d83c4..047d0f9a9e695 100644 --- a/scripts/scenarios/utf16-merge-conflict-blocks-commit.e2e.ts +++ b/scripts/scenarios/utf16-merge-conflict-blocks-commit.e2e.ts @@ -5,7 +5,7 @@ import * as FileSystem from "effect/FileSystem"; import { run } from "./shell.ts"; import { runPromise, test, WEB } from "./t3.ts"; -const utf16 = (text: string) => Buffer.from(`${text}`, "utf16le"); +const utf16 = (text: string) => Buffer.from(`\ufeff${text}`, "utf16le"); test( "The commit dialog refuses a merge whose UTF-16 file still has conflict markers", From c7fa686dade52af5dc8e0f8fb89c4622788acc21 Mon Sep 17 00:00:00 2001 From: Wout Stiens <71498452+StiensWout@users.noreply.github.com> Date: Sat, 14 Nov 2026 17:16:43 +0100 Subject: [PATCH 4/5] chore(server): clarify conflict guard comments and test names Co-authored-by: GPT 6.1 Sol Co-authored-by: Claude Opus 5.5 --- apps/server/src/vcs/GitVcsDriverCore.test.ts | 2 +- apps/server/src/vcs/GitVcsDriverCore.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/apps/server/src/vcs/GitVcsDriverCore.test.ts b/apps/server/src/vcs/GitVcsDriverCore.test.ts index 6afa75e426b1f..0e7799e359fc3 100644 --- a/apps/server/src/vcs/GitVcsDriverCore.test.ts +++ b/apps/server/src/vcs/GitVcsDriverCore.test.ts @@ -7406,7 +7406,7 @@ it.layer(layerTest)("GitVcsDriver core integration", (it) => { { location: "root", conflict: true, encoding: "utf16le", markerSize: 10 }, { location: "root", conflict: true, encoding: "utf16le", large: true }, ])( - "rejects selected-file merge commits without mutation ($location, conflict: $conflict, $encoding, marker size: $markerSize, large: $large)", + "rejects selected-file merge commits without mutation ($location, conflict: $conflict, $encoding)", ({ location, conflict, encoding, markerSize, large }) => Effect.gen(function* () { let cwd = yield* makeTmpDir(); diff --git a/apps/server/src/vcs/GitVcsDriverCore.ts b/apps/server/src/vcs/GitVcsDriverCore.ts index a81198e103e32..bf58b74b6f067 100644 --- a/apps/server/src/vcs/GitVcsDriverCore.ts +++ b/apps/server/src/vcs/GitVcsDriverCore.ts @@ -2709,7 +2709,7 @@ export const makeGitVcsDriverCore = Effect.fn("makeGitVcsDriverCore")(function* if (markerLines.size === 0) continue; emptyTree ??= yield* readEmptyTreeHash(cwd); // --check numbers lines after working-tree-encoding and clean filters; a diff from the - // empty tree prints that same view of the file. Read it in full, as the raw read did. + // empty tree prints that view. Read it in full so large resolved files can still commit. const fileDiff = yield* runGitStdoutWithOptions( "GitVcsDriver.commit.conflictMarkers", cwd, From 6a9fc2d2df94b0c0c09f86ddeae939be9c71c0e0 Mon Sep 17 00:00:00 2001 From: Julius Marminge Date: Sat, 14 Nov 2026 16:26:21 +0000 Subject: [PATCH 5/5] test(server): drop conflict-marker-size row unrelated to encoding Co-Authored-By: Claude Opus 5.5 --- apps/server/src/vcs/GitVcsDriverCore.test.ts | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/apps/server/src/vcs/GitVcsDriverCore.test.ts b/apps/server/src/vcs/GitVcsDriverCore.test.ts index 0e7799e359fc3..8f9fd37f110eb 100644 --- a/apps/server/src/vcs/GitVcsDriverCore.test.ts +++ b/apps/server/src/vcs/GitVcsDriverCore.test.ts @@ -7403,11 +7403,10 @@ it.layer(layerTest)("GitVcsDriver core integration", (it) => { { location: "nested", conflict: true, encoding: "utf8" }, { location: "worktree", conflict: true, encoding: "utf8" }, { location: "root", conflict: true, encoding: "utf16le" }, - { location: "root", conflict: true, encoding: "utf16le", markerSize: 10 }, { location: "root", conflict: true, encoding: "utf16le", large: true }, ])( "rejects selected-file merge commits without mutation ($location, conflict: $conflict, $encoding)", - ({ location, conflict, encoding, markerSize, large }) => + ({ location, conflict, encoding, large }) => Effect.gen(function* () { let cwd = yield* makeTmpDir(); const { initialBranch } = yield* initRepoWithCommit(cwd); @@ -7422,7 +7421,7 @@ it.layer(layerTest)("GitVcsDriver core integration", (it) => { yield* writeTextFile( cwd, ".gitattributes", - `README.md text working-tree-encoding=UTF-16${markerSize ? ` conflict-marker-size=${markerSize}` : ""}\n`, + "README.md text working-tree-encoding=UTF-16\n", ); yield* writeReadme("base\n"); yield* git(cwd, ["add", "."]);