From e2effba81bf509073b94c55db270289441d6a21f Mon Sep 17 00:00:00 2001 From: Julius Marminge Date: Thu, 12 Nov 2026 09:53:18 +0000 Subject: [PATCH 1/3] test(server): committing all files during an unresolved merge conflict is rejected Fails on main: the commit stages with git add -A, which marks unmerged paths resolved and commits the conflict markers (#35095). AI-assisted; verified with the tests and tools listed above. --- apps/server/src/vcs/GitVcsDriverCore.test.ts | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/apps/server/src/vcs/GitVcsDriverCore.test.ts b/apps/server/src/vcs/GitVcsDriverCore.test.ts index 0fce1ca55f521..ccfb16c5dfe10 100644 --- a/apps/server/src/vcs/GitVcsDriverCore.test.ts +++ b/apps/server/src/vcs/GitVcsDriverCore.test.ts @@ -6400,7 +6400,15 @@ 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); - if (conflict) yield* writeTextFile(cwd, "README.md", "resolved version\n"); + if (conflict) { + const unresolved = yield* driver + .commit(commitCwd, "Unresolved merge", "", { stage: {} }) + .pipe(Effect.result); + 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); + yield* writeTextFile(cwd, "README.md", "resolved version\n"); + } yield* driver.commit(commitCwd, "Finish merge", "", { stage: {} }); assert.equal( yield* git(cwd, ["show", "-s", "--format=%P", "HEAD"]), From 6d662905ca24a2891172c96f3ff018ef10f92ddd Mon Sep 17 00:00:00 2001 From: Julius Marminge Date: Thu, 12 Nov 2026 10:00:33 +0000 Subject: [PATCH 2/3] fix(server): Commit refuses while a merge still has conflict markers Committing all files staged with git add -A, which marks unmerged paths resolved, so the commit concluded the merge with the conflict markers in the file. Before staging, check the unmerged paths with git diff --check and refuse, naming the files, while any still holds a conflict marker. Fixes pingdotgg/t3code#35095 AI-assisted; verified with the tests and tools listed above. --- apps/server/src/vcs/GitVcsDriverCore.ts | 46 +++++++++++++++++++++++++ 1 file changed, 46 insertions(+) diff --git a/apps/server/src/vcs/GitVcsDriverCore.ts b/apps/server/src/vcs/GitVcsDriverCore.ts index e04bd48a8c843..b2222a09b4163 100644 --- a/apps/server/src/vcs/GitVcsDriverCore.ts +++ b/apps/server/src/vcs/GitVcsDriverCore.ts @@ -2526,6 +2526,52 @@ export const makeGitVcsDriverCore = Effect.fn("makeGitVcsDriverCore")(function* env ? { env } : {}, ); } else { + // add -A marks unmerged paths resolved, so refuse while one still has conflict markers. + const unmergedPaths = (yield* runGitStdoutWithOptions( + "GitVcsDriver.commit.unmergedPaths", + cwd, + ["-c", "diff.relative=false", "diff", "--name-only", "--diff-filter=U", "-z"], + env ? { env } : {}, + )) + .split("\0") + .filter((file) => file.length > 0); + if (unmergedPaths.length > 0) { + const check = yield* executeGit( + "GitVcsDriver.commit.conflictMarkers", + cwd, + [ + "-c", + "diff.relative=false", + "diff", + "--check", + "--no-color", + "--", + ...unmergedPaths.map((file) => `:(top,literal)${file}`), + ], + { + ...(env ? { env } : {}), + allowNonZeroExit: true, + maxOutputBytes: REVIEW_METADATA_MAX_OUTPUT_BYTES, + appendTruncationMarker: true, + }, + ); + const markerLines = check.stdout + .split("\n") + .filter((line) => line.endsWith(": leftover conflict marker")); + if (markerLines.length > 0) { + const conflicted = unmergedPaths.filter((file) => + markerLines.some((line) => line.startsWith(`${file}:`)), + ); + return yield* new GitCommandError({ + operation: "GitVcsDriver.commit", + command: "git commit", + cwd, + detail: `Cannot commit with unresolved merge conflicts${ + conflicted.length > 0 ? ` in ${conflicted.join(", ")}` : "" + }. Resolve them, then commit.`, + }); + } + } yield* runGit( "GitVcsDriver.commit.addAll", cwd, From e4f909c46ca00576637a2846162e0d6c52d4538c Mon Sep 17 00:00:00 2001 From: Julius Marminge Date: Thu, 12 Nov 2026 10:12:34 +0000 Subject: [PATCH 3/3] fix(server): a lone "=======" line no longer blocks finishing a merge git diff --check reports a standalone "=======" as a leftover conflict marker, so a resolved file with a reStructuredText heading underline was refused. Treat a file as still conflicted only when its reported marker lines include both an opening "<" and a closing ">" boundary. AI-assisted; verified with the tests and tools listed above. --- apps/server/src/vcs/GitVcsDriverCore.test.ts | 9 +++- apps/server/src/vcs/GitVcsDriverCore.ts | 44 ++++++++++++++++---- 2 files changed, 42 insertions(+), 11 deletions(-) diff --git a/apps/server/src/vcs/GitVcsDriverCore.test.ts b/apps/server/src/vcs/GitVcsDriverCore.test.ts index ccfb16c5dfe10..e52508a5c6b89 100644 --- a/apps/server/src/vcs/GitVcsDriverCore.test.ts +++ b/apps/server/src/vcs/GitVcsDriverCore.test.ts @@ -6407,7 +6407,9 @@ it.layer(layerTest)("GitVcsDriver core integration", (it) => { 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); - yield* writeTextFile(cwd, "README.md", "resolved version\n"); + // A lone "=======" is a heading underline, not a conflict boundary. + yield* writeTextFile(cwd, "README.md", "Heading\n=======\n\nresolved\n"); + assert.isNotNull(yield* driver.prepareCommitContext(commitCwd)); } yield* driver.commit(commitCwd, "Finish merge", "", { stage: {} }); assert.equal( @@ -6418,7 +6420,10 @@ it.layer(layerTest)("GitVcsDriver core integration", (it) => { assert.equal(yield* git(cwd, ["show", "HEAD:feature.txt"]), "feature"); assert.equal(yield* git(cwd, ["show", "HEAD:unrelated.txt"]), "unrelated"); if (conflict) - assert.equal(yield* git(cwd, ["show", "HEAD:README.md"]), "resolved version"); + 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 b2222a09b4163..0fb169a54efbe 100644 --- a/apps/server/src/vcs/GitVcsDriverCore.ts +++ b/apps/server/src/vcs/GitVcsDriverCore.ts @@ -2555,20 +2555,46 @@ export const makeGitVcsDriverCore = Effect.fn("makeGitVcsDriverCore")(function* appendTruncationMarker: true, }, ); - const markerLines = check.stdout - .split("\n") - .filter((line) => line.endsWith(": leftover conflict marker")); - if (markerLines.length > 0) { - const conflicted = unmergedPaths.filter((file) => - markerLines.some((line) => line.startsWith(`${file}:`)), + const reports = check.stdout.split("\n"); + const conflicted: string[] = []; + let topLevel: string | undefined; + for (const file of unmergedPaths) { + const markerLines = new Set( + reports.flatMap((line) => { + const match = line.startsWith(`${file}:`) + ? /^(\d+): leftover conflict marker$/.exec(line.slice(file.length + 1)) + : null; + return match ? [Number(match[1]) - 1] : []; + }), ); + 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( + Effect.mapError( + (cause) => + new GitCommandError({ + operation: "GitVcsDriver.commit.conflictMarkers", + command: "git commit", + cwd, + detail: `Could not check ${file} for conflict markers.`, + cause, + }), + ), + )).split("\n"); + // 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)); + if (reported("<") && reported(">")) conflicted.push(file); + } + if (conflicted.length > 0) { return yield* new GitCommandError({ operation: "GitVcsDriver.commit", command: "git commit", cwd, - detail: `Cannot commit with unresolved merge conflicts${ - conflicted.length > 0 ? ` in ${conflicted.join(", ")}` : "" - }. Resolve them, then commit.`, + detail: `Cannot commit with unresolved merge conflicts in ${conflicted.join(", ")}. Resolve them, then commit.`, }); } }