From 1d107bd84ac5d91d9c9754e728c098530c1be93b Mon Sep 17 00:00:00 2001 From: Julius Marminge Date: Fri, 19 Jun 2026 23:25:11 -0700 Subject: [PATCH 1/2] Fix PR base for origin worktrees --- apps/server/src/server.test.ts | 1 + apps/server/src/vcs/GitVcsDriverCore.test.ts | 8 +++++++ apps/server/src/vcs/GitVcsDriverCore.ts | 22 ++++++++++++++++---- apps/server/src/ws.ts | 1 + packages/contracts/src/git.test.ts | 12 +++++++++++ packages/contracts/src/git.ts | 1 + 6 files changed, 41 insertions(+), 4 deletions(-) diff --git a/apps/server/src/server.test.ts b/apps/server/src/server.test.ts index 1529285e50c..76824af73e3 100644 --- a/apps/server/src/server.test.ts +++ b/apps/server/src/server.test.ts @@ -6041,6 +6041,7 @@ it.layer(NodeServices.layer)("server router seam", (it) => { cwd: "/tmp/project", refName: fetchedOriginCommit, newRefName: "t3code/bootstrap-refName", + baseRefName: "main", path: null, }); assert.deepEqual(fetchRemote.mock.calls[0]?.[0], { diff --git a/apps/server/src/vcs/GitVcsDriverCore.test.ts b/apps/server/src/vcs/GitVcsDriverCore.test.ts index 5be6427fe73..2fd4d447c58 100644 --- a/apps/server/src/vcs/GitVcsDriverCore.test.ts +++ b/apps/server/src/vcs/GitVcsDriverCore.test.ts @@ -568,13 +568,21 @@ it.layer(TestLayer)("GitVcsDriver core integration", (it) => { path: worktreePath, refName: resolvedBase.commitSha, newRefName: "t3code/fetched-origin", + baseRefName: resolvedBase.remoteRefName, }); assert.equal(yield* git(worktreePath, ["rev-parse", "HEAD"]), remoteHead); + assert.equal( + yield* driver.readConfigValue(worktreePath, "branch.t3code/fetched-origin.gh-merge-base"), + initialBranch, + ); assert.equal( yield* driver.readConfigValue(worktreePath, "branch.t3code/fetched-origin.remote"), null, ); + const status = yield* driver.statusDetails(worktreePath); + assert.equal(status.aheadCount, 0); + assert.equal(status.aheadOfDefaultCount, 0); }), ); diff --git a/apps/server/src/vcs/GitVcsDriverCore.ts b/apps/server/src/vcs/GitVcsDriverCore.ts index 0e8f8df16e2..23a968a9cfe 100644 --- a/apps/server/src/vcs/GitVcsDriverCore.ts +++ b/apps/server/src/vcs/GitVcsDriverCore.ts @@ -1130,16 +1130,16 @@ export const makeGitVcsDriverCore = Effect.fn("makeGitVcsDriverCore")(function* continue; } - if (yield* branchExists(cwd, normalizedCandidate)) { - return normalizedCandidate; - } - if ( primaryRemoteName && (yield* remoteBranchExists(cwd, primaryRemoteName, normalizedCandidate)) ) { return `${primaryRemoteName}/${normalizedCandidate}`; } + + if (yield* branchExists(cwd, normalizedCandidate)) { + return normalizedCandidate; + } } return null; @@ -2178,6 +2178,20 @@ export const makeGitVcsDriverCore = Effect.fn("makeGitVcsDriverCore")(function* fallbackErrorMessage: "git worktree add failed", }); + if (input.newRefName && input.baseRefName) { + const remoteNames = yield* listRemoteNames(input.cwd).pipe(Effect.orElseSucceed(() => [])); + const parsedBaseRef = parseRemoteRefWithRemoteNames( + input.baseRefName, + remoteNames.toSorted((left, right) => right.length - left.length), + ); + const baseBranch = parsedBaseRef?.branchName ?? input.baseRefName; + yield* runGit("GitVcsDriver.createWorktree.configureBaseRef", input.cwd, [ + "config", + `branch.${input.newRefName}.gh-merge-base`, + baseBranch, + ]); + } + return { worktree: { path: worktreePath, diff --git a/apps/server/src/ws.ts b/apps/server/src/ws.ts index 7eb4ba882d9..0b25b25f6f6 100644 --- a/apps/server/src/ws.ts +++ b/apps/server/src/ws.ts @@ -712,6 +712,7 @@ const makeWsRpcLayer = (currentSession: EnvironmentAuth.AuthenticatedSession) => cwd: bootstrap.prepareWorktree.projectCwd, refName: worktreeBaseRef, newRefName: bootstrap.prepareWorktree.branch, + baseRefName: bootstrap.prepareWorktree.baseBranch, path: null, }); targetWorktreePath = worktree.worktree.path; diff --git a/packages/contracts/src/git.test.ts b/packages/contracts/src/git.test.ts index 0a5497367cd..4ea86670ff8 100644 --- a/packages/contracts/src/git.test.ts +++ b/packages/contracts/src/git.test.ts @@ -28,6 +28,18 @@ describe("VcsCreateWorktreeInput", () => { expect(parsed.newRefName).toBeUndefined(); expect(parsed.refName).toBe("feature/existing"); }); + + it("accepts baseRefName metadata for a new worktree ref", () => { + const parsed = decodeCreateWorktreeInput({ + cwd: "/repo", + refName: "0123456789abcdef", + newRefName: "feature/new", + baseRefName: "origin/main", + path: "/tmp/worktree", + }); + + expect(parsed.baseRefName).toBe("origin/main"); + }); }); describe("GitPreparePullRequestThreadInput", () => { diff --git a/packages/contracts/src/git.ts b/packages/contracts/src/git.ts index 3de6c84fa44..7ee2a571963 100644 --- a/packages/contracts/src/git.ts +++ b/packages/contracts/src/git.ts @@ -137,6 +137,7 @@ export const VcsCreateWorktreeInput = Schema.Struct({ cwd: TrimmedNonEmptyStringSchema, refName: TrimmedNonEmptyStringSchema, newRefName: Schema.optional(TrimmedNonEmptyStringSchema), + baseRefName: Schema.optional(TrimmedNonEmptyStringSchema), path: Schema.NullOr(TrimmedNonEmptyStringSchema), }); export type VcsCreateWorktreeInput = typeof VcsCreateWorktreeInput.Type; From e0302936962fe16ae71fd328753ac05ea55d5fba Mon Sep 17 00:00:00 2001 From: Julius Marminge Date: Fri, 19 Jun 2026 23:34:27 -0700 Subject: [PATCH 2/2] Use remote base for PR content range --- apps/server/src/git/GitManager.test.ts | 56 ++++++++++++++++++++++++++ apps/server/src/git/GitManager.ts | 24 ++++++++++- 2 files changed, 79 insertions(+), 1 deletion(-) diff --git a/apps/server/src/git/GitManager.test.ts b/apps/server/src/git/GitManager.test.ts index 165c351b36c..2b296e5f3fa 100644 --- a/apps/server/src/git/GitManager.test.ts +++ b/apps/server/src/git/GitManager.test.ts @@ -2239,6 +2239,62 @@ it.layer(GitManagerTestLayer)("GitManager", (it) => { }), ); + it.effect("generates PR content against the remote base when the local base is stale", () => + Effect.gen(function* () { + const repoDir = yield* makeTempDir("t3code-git-manager-"); + yield* initRepo(repoDir); + const remoteDir = yield* createBareRemote(); + yield* runGit(repoDir, ["remote", "add", "origin", remoteDir]); + yield* runGit(repoDir, ["push", "-u", "origin", "main"]); + yield* runGit(remoteDir, ["symbolic-ref", "HEAD", "refs/heads/main"]); + + const peerDir = yield* makeTempDir("t3code-git-peer-"); + yield* runGit(peerDir, ["clone", remoteDir, "."]); + yield* runGit(peerDir, ["config", "user.email", "peer@example.com"]); + yield* runGit(peerDir, ["config", "user.name", "Peer User"]); + fs.writeFileSync(path.join(peerDir, "remote.txt"), "remote\n"); + yield* runGit(peerDir, ["add", "remote.txt"]); + yield* runGit(peerDir, ["commit", "-m", "Remote base commit"]); + yield* runGit(peerDir, ["push", "origin", "main"]); + + yield* runGit(repoDir, ["fetch", "origin"]); + yield* runGit(repoDir, [ + "checkout", + "--no-track", + "-b", + "feature/remote-base", + "origin/main", + ]); + fs.writeFileSync(path.join(repoDir, "feature.txt"), "feature\n"); + yield* runGit(repoDir, ["add", "feature.txt"]); + yield* runGit(repoDir, ["commit", "-m", "Feature commit"]); + yield* runGit(repoDir, ["push", "-u", "origin", "feature/remote-base"]); + yield* runGit(repoDir, ["config", "branch.feature/remote-base.gh-merge-base", "main"]); + + let generatedCommitSummary = ""; + const { manager } = yield* makeManager({ + ghScenario: { + prListSequence: ["[]", "[]"], + }, + textGeneration: { + generatePrContent: (input) => { + generatedCommitSummary = input.commitSummary; + return Effect.succeed({ title: "Feature PR", body: "Feature body" }); + }, + }, + }); + + const result = yield* runStackedAction(manager, { + cwd: repoDir, + action: "create_pr", + }); + + expect(result.pr.status).toBe("created"); + expect(generatedCommitSummary).toContain("Feature commit"); + expect(generatedCommitSummary).not.toContain("Remote base commit"); + }), + ); + it.effect( "creates a new PR instead of reusing an unrelated fork PR with the same head branch", () => diff --git a/apps/server/src/git/GitManager.ts b/apps/server/src/git/GitManager.ts index 9938c40cffb..c57c814f437 100644 --- a/apps/server/src/git/GitManager.ts +++ b/apps/server/src/git/GitManager.ts @@ -1092,6 +1092,27 @@ export const make = Effect.gen(function* () { return "main"; }); + const resolveBaseRangeRef = Effect.fn("resolveBaseRangeRef")(function* ( + cwd: string, + baseBranch: string, + ) { + const remoteName = yield* gitCore + .resolvePrimaryRemoteName(cwd) + .pipe(Effect.orElseSucceed(() => null)); + if (!remoteName) return baseBranch; + + return yield* gitCore + .resolveRemoteTrackingCommit({ + cwd, + refName: baseBranch, + fallbackRemoteName: remoteName, + }) + .pipe( + Effect.map((resolved) => resolved.commitSha), + Effect.orElseSucceed(() => baseBranch), + ); + }); + const resolveCommitAndBranchSuggestion = Effect.fn("resolveCommitAndBranchSuggestion")( function* (input: { cwd: string; @@ -1298,7 +1319,8 @@ export const make = Effect.gen(function* () { phase: "pr", label: `Generating ${terms.shortLabel} content...`, }); - const rangeContext = yield* gitCore.readRangeContext(cwd, baseBranch); + const baseRangeRef = yield* resolveBaseRangeRef(cwd, baseBranch); + const rangeContext = yield* gitCore.readRangeContext(cwd, baseRangeRef); const generated = yield* textGeneration.generatePrContent({ cwd,