diff --git a/apps/server/src/git/GitManager.test.ts b/apps/server/src/git/GitManager.test.ts index cab5020d7423..9fcdb041f489 100644 --- a/apps/server/src/git/GitManager.test.ts +++ b/apps/server/src/git/GitManager.test.ts @@ -1019,50 +1019,58 @@ it.layer(GitManagerTestLayer)("GitManager", (it) => { }), ); - it.effect("turn-end refresh finds a new PR and keeps known PRs cached", () => - 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(repoDir, ["checkout", "-b", "feature/turn-refresh", "origin/main"]); - yield* runGit(repoDir, ["push", "origin", "feature/turn-refresh"]); + it.effect.each(["main", "dev"])( + "turn-end refresh finds a new PR and keeps known PRs cached while tracking origin/%s", + (baseBranch) => + 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"]); + if (baseBranch !== "main") { + yield* runGit(repoDir, ["branch", baseBranch]); + yield* runGit(repoDir, ["push", "origin", baseBranch]); + } + yield* runGit(repoDir, ["checkout", "-b", "feature/turn-refresh", `origin/${baseBranch}`]); + yield* runGit(repoDir, ["push", "origin", "feature/turn-refresh"]); - const { manager, ghCalls } = yield* makeManager({ - ghScenario: { - prListSequence: [ - "[]", - // Fake gh returns raw JSON stdout, matching the CLI boundary under test. - // @effect-diagnostics-next-line preferSchemaOverJson:off - JSON.stringify([ - { - number: 114, - title: "Opened during the turn", - url: "https://github.com/pingdotgg/codething-mvp/pull/114", - baseRefName: "main", - headRefName: "feature/turn-refresh", - }, - ]), - ], - }, - }); - expect((yield* manager.remoteStatus({ cwd: repoDir }))?.pr).toBeNull(); - expect( - (yield* manager.remoteStatus({ cwd: repoDir }, { refreshUpstream: false }))?.pr, - ).toBeNull(); + const { manager, ghCalls } = yield* makeManager({ + ghScenario: { + prListSequenceByHeadSelector: { + "feature/turn-refresh": [ + "[]", + // Fake gh returns raw JSON stdout, matching the CLI boundary under test. + // @effect-diagnostics-next-line preferSchemaOverJson:off + JSON.stringify([ + { + number: 114, + title: "Opened during the turn", + url: "https://github.com/pingdotgg/codething-mvp/pull/114", + baseRefName: baseBranch, + headRefName: "feature/turn-refresh", + }, + ]), + ], + }, + }, + }); + expect((yield* manager.remoteStatus({ cwd: repoDir }))?.pr).toBeNull(); + expect( + (yield* manager.remoteStatus({ cwd: repoDir }, { refreshUpstream: false }))?.pr, + ).toBeNull(); - const refreshed = yield* manager.remoteStatus( - { cwd: repoDir }, - { refreshUpstream: false, refreshMissingPullRequest: true }, - ); - expect(refreshed?.pr?.number).toBe(114); - yield* manager.remoteStatus( - { cwd: repoDir }, - { refreshUpstream: false, refreshMissingPullRequest: true }, - ); - expect(ghCalls.filter((call) => call.startsWith("pr list "))).toHaveLength(2); - }), + const refreshed = yield* manager.remoteStatus( + { cwd: repoDir }, + { refreshUpstream: false, refreshMissingPullRequest: true }, + ); + expect(refreshed?.pr?.number).toBe(114); + yield* manager.remoteStatus( + { cwd: repoDir }, + { refreshUpstream: false, refreshMissingPullRequest: true }, + ); + expect(ghCalls.filter((call) => call.startsWith("pr list "))).toHaveLength(2); + }), ); it.effect("turn-end refresh preserves failed PR lookup backoff", () => @@ -2223,7 +2231,88 @@ it.layer(GitManagerTestLayer)("GitManager", (it) => { }), ); - it.effect("status finds a PR pushed under the branch's own name despite a default upstream", () => + it.effect( + "status does not inherit a merged PR from a feature branch's non-default upstream", + () => + 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(repoDir, ["remote", "set-head", "origin", "main"]); + yield* runGit(repoDir, ["checkout", "-b", "dev"]); + yield* runGit(repoDir, ["push", "-u", "origin", "dev"]); + yield* runGit(repoDir, ["checkout", "-b", "feature/from-dev", "origin/dev"]); + + const { manager, ghCalls } = yield* makeManager({ + ghScenario: { + prListSequence: [ + // @effect-diagnostics-next-line preferSchemaOverJson:off + JSON.stringify([ + { + number: 49, + title: "Dev", + url: "https://github.com/pingdotgg/codething-mvp/pull/49", + baseRefName: "main", + headRefName: "dev", + state: "MERGED", + mergedAt: "2026-08-25T11:42:51Z", + updatedAt: "2026-08-25T11:42:51Z", + }, + ]), + ], + }, + }); + + const status = yield* manager.status({ cwd: repoDir }); + expect(status.refName).toBe("feature/from-dev"); + expect(status.pr).toBeNull(); + expect(ghCalls.some((call) => call.includes("pr list"))).toBe(false); + }), + ); + + it.effect( + "status does not inherit a PR from a hierarchical base whose tail matches the branch name", + () => + 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(repoDir, ["remote", "set-head", "origin", "main"]); + yield* runGit(repoDir, ["checkout", "-b", "release/v2"]); + yield* runGit(repoDir, ["push", "-u", "origin", "release/v2"]); + yield* runGit(repoDir, ["checkout", "-b", "v2", "origin/release/v2"]); + + const { manager, ghCalls } = yield* makeManager({ + ghScenario: { + prListSequence: [ + // @effect-diagnostics-next-line preferSchemaOverJson:off + JSON.stringify([ + { + number: 61, + title: "Release v2", + url: "https://github.com/pingdotgg/codething-mvp/pull/61", + baseRefName: "main", + headRefName: "release/v2", + state: "OPEN", + updatedAt: "2026-08-20T09:00:00Z", + }, + ]), + ], + }, + }); + + const status = yield* manager.status({ cwd: repoDir }); + expect(status.refName).toBe("v2"); + expect(status.pr).toBeNull(); + expect(ghCalls.some((call) => call.includes("pr list"))).toBe(false); + }), + ); + + it.effect("status does not inherit a PR from a base that is the tail of the branch name", () => Effect.gen(function* () { const repoDir = yield* makeTempDir("t3code-git-manager-"); yield* initRepo(repoDir); @@ -2231,40 +2320,87 @@ it.layer(GitManagerTestLayer)("GitManager", (it) => { yield* runGit(repoDir, ["remote", "add", "origin", remoteDir]); yield* runGit(repoDir, ["push", "-u", "origin", "main"]); yield* runGit(repoDir, ["remote", "set-head", "origin", "main"]); - yield* runGit(repoDir, ["checkout", "-b", "feature/pushed-plain", "origin/main"]); - // A plain push (no -u) leaves the upstream on origin/main. - yield* runGit(repoDir, ["push", "origin", "feature/pushed-plain"]); + yield* runGit(repoDir, ["checkout", "-b", "dev"]); + yield* runGit(repoDir, ["push", "-u", "origin", "dev"]); + yield* runGit(repoDir, ["checkout", "-b", "feature/dev", "origin/dev"]); const { manager, ghCalls } = yield* makeManager({ ghScenario: { - prListByHeadSelector: { - // Fake gh returns raw JSON stdout, matching the CLI boundary under test. + prListSequence: [ // @effect-diagnostics-next-line preferSchemaOverJson:off - "feature/pushed-plain": JSON.stringify([ + JSON.stringify([ { - number: 88, - title: "Pushed without -u", - url: "https://github.com/pingdotgg/codething-mvp/pull/88", + number: 62, + title: "Dev", + url: "https://github.com/pingdotgg/codething-mvp/pull/62", baseRefName: "main", - headRefName: "feature/pushed-plain", + headRefName: "dev", state: "OPEN", - updatedAt: "2026-05-01T10:00:00Z", + updatedAt: "2026-08-20T09:00:00Z", }, ]), - }, + ], }, }); const status = yield* manager.status({ cwd: repoDir }); - expect(status.refName).toBe("feature/pushed-plain"); - expect(status.pr?.number).toBe(88); - expect(ghCalls.some((call) => call.includes("--head main"))).toBe(false); + expect(status.refName).toBe("feature/dev"); + expect(status.pr).toBeNull(); + expect(ghCalls.some((call) => call.includes("pr list"))).toBe(false); }), ); - it.effect( - "status finds a fork PR pushed under the branch's own name despite a default upstream", - () => + it.effect.each(["main", "dev"])( + "status finds a PR pushed under the branch's own name while tracking origin/%s", + (baseBranch) => + 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"]); + if (baseBranch !== "main") { + yield* runGit(repoDir, ["branch", baseBranch]); + yield* runGit(repoDir, ["push", "origin", baseBranch]); + } + yield* runGit(repoDir, ["remote", "set-head", "origin", "main"]); + yield* runGit(repoDir, ["checkout", "-b", "feature/pushed-plain", `origin/${baseBranch}`]); + // A plain push (no -u) leaves the upstream on the base branch. + yield* runGit(repoDir, ["push", "origin", "feature/pushed-plain"]); + + const { manager, ghCalls } = yield* makeManager({ + ghScenario: { + prListByHeadSelector: { + // Fake gh returns raw JSON stdout, matching the CLI boundary under test. + // @effect-diagnostics-next-line preferSchemaOverJson:off + "feature/pushed-plain": JSON.stringify([ + { + number: 88, + title: "Pushed without -u", + url: "https://github.com/pingdotgg/codething-mvp/pull/88", + baseRefName: baseBranch, + headRefName: "feature/pushed-plain", + state: "OPEN", + updatedAt: "2026-05-01T10:00:00Z", + }, + ]), + }, + }, + }); + + expect( + (yield* runGit(repoDir, ["rev-parse", "--abbrev-ref", "@{upstream}"])).stdout.trim(), + ).toBe(`origin/${baseBranch}`); + const status = yield* manager.status({ cwd: repoDir }); + expect(status.refName).toBe("feature/pushed-plain"); + expect(status.pr?.number).toBe(88); + expect(ghCalls.some((call) => call.includes(`--head ${baseBranch}`))).toBe(false); + }), + ); + + it.effect.each(["main", "dev"])( + "status finds a fork PR pushed under the branch's own name while tracking origin/%s", + (baseBranch) => Effect.gen(function* () { const repoDir = yield* makeTempDir("t3code-git-manager-"); yield* initRepo(repoDir); @@ -2272,10 +2408,14 @@ it.layer(GitManagerTestLayer)("GitManager", (it) => { const forkDir = yield* createBareRemote(); yield* runGit(repoDir, ["remote", "add", "origin", originDir]); yield* runGit(repoDir, ["push", "-u", "origin", "main"]); + if (baseBranch !== "main") { + yield* runGit(repoDir, ["branch", baseBranch]); + yield* runGit(repoDir, ["push", "origin", baseBranch]); + } yield* runGit(repoDir, ["remote", "set-head", "origin", "main"]); yield* configureRemote(repoDir, "team/fork", forkDir, "team/fork"); - yield* runGit(repoDir, ["checkout", "-b", "feature/fork-plain", "origin/main"]); - // Pushed to the fork without -u: upstream stays origin/main. + yield* runGit(repoDir, ["checkout", "-b", "feature/fork-plain", `origin/${baseBranch}`]); + // Pushed to the fork without -u: upstream stays on the base branch. yield* runGit(repoDir, ["push", "team/fork", "feature/fork-plain"]); yield* configureVisibleRemoteUrlWithLocalRewrite( repoDir, @@ -2300,7 +2440,7 @@ it.layer(GitManagerTestLayer)("GitManager", (it) => { number: 89, title: "Fork PR pushed without -u", url: "https://github.com/pingdotgg/codething-mvp/pull/89", - baseRefName: "main", + baseRefName: baseBranch, headRefName: "feature/fork-plain", state: "OPEN", updatedAt: "2026-05-01T10:00:00Z", @@ -2318,116 +2458,128 @@ it.layer(GitManagerTestLayer)("GitManager", (it) => { expect(ghCalls.some((call) => call.includes("--head contributor:feature/fork-plain"))).toBe( true, ); - expect(ghCalls.some((call) => call.includes("--head main"))).toBe(false); + expect(ghCalls.some((call) => call.includes(`--head ${baseBranch}`))).toBe(false); }), ); - it.effect("branch PR lookup verifies identity on the fork that holds the own-name ref", () => - Effect.gen(function* () { - const repoDir = yield* makeTempDir("t3code-git-manager-"); - yield* initRepo(repoDir); - const originDir = yield* createBareRemote(); - const forkDir = yield* createBareRemote(); - yield* runGit(repoDir, ["remote", "add", "origin", originDir]); - yield* runGit(repoDir, ["push", "-u", "origin", "main"]); - yield* runGit(repoDir, ["remote", "set-head", "origin", "main"]); - yield* configureRemote(repoDir, "team/fork", forkDir, "team/fork"); - yield* runGit(repoDir, ["checkout", "-b", "feature/fork-settle", "origin/main"]); - yield* runGit(repoDir, ["push", "team/fork", "feature/fork-settle"]); - yield* configureVisibleRemoteUrlWithLocalRewrite( - repoDir, - "origin", - "git@github.com:pingdotgg/codething-mvp.git", - originDir, - ); - yield* configureVisibleRemoteUrlWithLocalRewrite( - repoDir, - "team/fork", - "git@github.com:contributor/codething-mvp.git", - forkDir, - ); + it.effect.each(["main", "dev"])( + "branch PR lookup verifies identity on the fork that holds the own-name ref while tracking origin/%s", + (baseBranch) => + Effect.gen(function* () { + const repoDir = yield* makeTempDir("t3code-git-manager-"); + yield* initRepo(repoDir); + const originDir = yield* createBareRemote(); + const forkDir = yield* createBareRemote(); + yield* runGit(repoDir, ["remote", "add", "origin", originDir]); + yield* runGit(repoDir, ["push", "-u", "origin", "main"]); + if (baseBranch !== "main") { + yield* runGit(repoDir, ["branch", baseBranch]); + yield* runGit(repoDir, ["push", "origin", baseBranch]); + } + yield* runGit(repoDir, ["remote", "set-head", "origin", "main"]); + yield* configureRemote(repoDir, "team/fork", forkDir, "team/fork"); + yield* runGit(repoDir, ["checkout", "-b", "feature/fork-settle", `origin/${baseBranch}`]); + yield* runGit(repoDir, ["push", "team/fork", "feature/fork-settle"]); + yield* configureVisibleRemoteUrlWithLocalRewrite( + repoDir, + "origin", + "git@github.com:pingdotgg/codething-mvp.git", + originDir, + ); + yield* configureVisibleRemoteUrlWithLocalRewrite( + repoDir, + "team/fork", + "git@github.com:contributor/codething-mvp.git", + forkDir, + ); - const { manager } = yield* makeManager({ - ghScenario: { - prListByHeadSelector: { - // Fake gh returns raw JSON stdout, matching the CLI boundary under test. - // @effect-diagnostics-next-line preferSchemaOverJson:off - "contributor:feature/fork-settle": JSON.stringify([ - { - number: 91, - title: "Fork PR to settle", - url: "https://github.com/pingdotgg/codething-mvp/pull/91", - baseRefName: "main", - headRefName: "feature/fork-settle", - state: "MERGED", - updatedAt: "2026-05-02T10:00:00Z", - isCrossRepository: true, - headRepository: { nameWithOwner: "contributor/codething-mvp" }, - headRepositoryOwner: { login: "contributor" }, - }, - ]), + const { manager } = yield* makeManager({ + ghScenario: { + prListByHeadSelector: { + // Fake gh returns raw JSON stdout, matching the CLI boundary under test. + // @effect-diagnostics-next-line preferSchemaOverJson:off + "contributor:feature/fork-settle": JSON.stringify([ + { + number: 91, + title: "Fork PR to settle", + url: "https://github.com/pingdotgg/codething-mvp/pull/91", + baseRefName: baseBranch, + headRefName: "feature/fork-settle", + state: "MERGED", + updatedAt: "2026-05-02T10:00:00Z", + isCrossRepository: true, + headRepository: { nameWithOwner: "contributor/codething-mvp" }, + headRepositoryOwner: { login: "contributor" }, + }, + ]), + }, }, - }, - }); + }); - const pullRequest = yield* manager.branchPullRequest({ - cwd: repoDir, - branch: "feature/fork-settle", - }); + const pullRequest = yield* manager.branchPullRequest({ + cwd: repoDir, + branch: "feature/fork-settle", + }); - expect(pullRequest).toMatchObject({ - state: "merged", - closedAt: null, - mergedAt: null, - updatedAt: "2026-05-02T10:00:00.000Z", - }); - }), + expect(pullRequest).toMatchObject({ + state: "merged", + closedAt: null, + mergedAt: null, + updatedAt: "2026-05-02T10:00:00.000Z", + }); + }), ); - it.effect("status keeps an own-name PR when a later lookup fails on a default upstream", () => - 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(repoDir, ["remote", "set-head", "origin", "main"]); - yield* runGit(repoDir, ["checkout", "-b", "feature/sticky-plain", "origin/main"]); - yield* runGit(repoDir, ["push", "origin", "feature/sticky-plain"]); + it.effect.each(["main", "dev"])( + "status keeps an own-name PR when a later lookup fails while tracking origin/%s", + (baseBranch) => + 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"]); + if (baseBranch !== "main") { + yield* runGit(repoDir, ["branch", baseBranch]); + yield* runGit(repoDir, ["push", "origin", baseBranch]); + } + yield* runGit(repoDir, ["remote", "set-head", "origin", "main"]); + yield* runGit(repoDir, ["checkout", "-b", "feature/sticky-plain", `origin/${baseBranch}`]); + yield* runGit(repoDir, ["push", "origin", "feature/sticky-plain"]); - const { manager } = yield* makeManager({ - ghScenario: { - prListByHeadSelector: { - // Fake gh returns raw JSON stdout, matching the CLI boundary under test. - // @effect-diagnostics-next-line preferSchemaOverJson:off - "feature/sticky-plain": JSON.stringify([ - { - number: 90, - title: "Sticky own-name PR", - url: "https://github.com/pingdotgg/codething-mvp/pull/90", - baseRefName: "main", - headRefName: "feature/sticky-plain", - state: "OPEN", - updatedAt: "2026-05-01T10:00:00Z", - }, - ]), + const { manager } = yield* makeManager({ + ghScenario: { + prListByHeadSelector: { + // Fake gh returns raw JSON stdout, matching the CLI boundary under test. + // @effect-diagnostics-next-line preferSchemaOverJson:off + "feature/sticky-plain": JSON.stringify([ + { + number: 90, + title: "Sticky own-name PR", + url: "https://github.com/pingdotgg/codething-mvp/pull/90", + baseRefName: baseBranch, + headRefName: "feature/sticky-plain", + state: "OPEN", + updatedAt: "2026-05-01T10:00:00Z", + }, + ]), + }, + failWith: new GitHubCli.GitHubCliUnavailableError({ + command: "gh", + cwd: repoDir, + cause: new Error("rate limited"), + }), + failAfterCalls: 1, }, - failWith: new GitHubCli.GitHubCliUnavailableError({ - command: "gh", - cwd: repoDir, - cause: new Error("rate limited"), - }), - failAfterCalls: 1, - }, - }); + }); - const first = yield* manager.status({ cwd: repoDir }); - expect(first.pr?.number).toBe(90); + const first = yield* manager.status({ cwd: repoDir }); + expect(first.pr?.number).toBe(90); - yield* manager.invalidateStatus(repoDir); - const second = yield* manager.status({ cwd: repoDir }); - expect(second.pr?.number).toBe(90); - }), + yield* manager.invalidateStatus(repoDir); + const second = yield* manager.status({ cwd: repoDir }); + expect(second.pr?.number).toBe(90); + }), ); it.effect("status prefers open PR when merged PR has newer updatedAt", () => @@ -3289,6 +3441,69 @@ it.layer(GitManagerTestLayer)("GitManager", (it) => { }), ); + it.effect.each(["main", "dev"])( + "push completion links the branch's own PR after a plain push while tracking origin/%s", + (baseBranch) => + 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(repoDir, ["remote", "set-head", "origin", "main"]); + if (baseBranch !== "main") { + yield* runGit(repoDir, ["branch", baseBranch]); + yield* runGit(repoDir, ["push", "origin", baseBranch]); + } + const branch = "feature/push-toast"; + yield* runGit(repoDir, ["checkout", "-b", branch, `origin/${baseBranch}`]); + NodeFS.writeFileSync(NodePath.join(repoDir, "feature.txt"), "feature\n"); + yield* runGit(repoDir, ["add", "feature.txt"]); + yield* runGit(repoDir, ["commit", "-m", "Add feature"]); + yield* runGit(repoDir, ["push", "origin", branch]); + expect( + (yield* runGit(repoDir, ["rev-parse", "--abbrev-ref", "@{upstream}"])).stdout.trim(), + ).toBe(`origin/${baseBranch}`); + + const prUrl = "https://github.com/pingdotgg/codething-mvp/pull/88"; + const { manager, ghCalls } = yield* makeManager({ + ghScenario: { + prListByHeadSelector: { + [baseBranch]: encodeCliJson([ + { + number: 99, + title: "Base branch PR", + url: "https://github.com/pingdotgg/codething-mvp/pull/99", + baseRefName: "release", + headRefName: baseBranch, + state: "OPEN", + }, + ]), + [branch]: encodeCliJson([ + { + number: 88, + title: "Feature PR", + url: prUrl, + baseRefName: baseBranch, + headRefName: branch, + state: "OPEN", + }, + ]), + }, + }, + }); + + const result = yield* runStackedAction(manager, { cwd: repoDir, action: "push" }); + + expect(result.push.status).toBe("pushed"); + expect( + (yield* runGit(repoDir, ["rev-parse", "--abbrev-ref", "@{upstream}"])).stdout.trim(), + ).toBe(`origin/${branch}`); + expect(result.toast.cta).toEqual({ kind: "open_pr", label: "View PR", url: prUrl }); + expect(ghCalls.some((call) => call.includes(`--head ${baseBranch}`))).toBe(false); + }), + ); + it.effect("pushes existing commits without committing dirty worktree changes", () => Effect.gen(function* () { const repoDir = yield* makeTempDir("t3code-git-manager-"); diff --git a/apps/server/src/git/GitManager.ts b/apps/server/src/git/GitManager.ts index 33f041fb6cd1..52fcfd28f1b1 100644 --- a/apps/server/src/git/GitManager.ts +++ b/apps/server/src/git/GitManager.ts @@ -1033,7 +1033,6 @@ export const make = Effect.gen(function* () { details: { branch: string; upstreamRef: string | null; - defaultBranch: string | null; localBranchExists?: boolean; remoteName?: string | null; }, @@ -1042,7 +1041,6 @@ export const make = Effect.gen(function* () { cwd, details.branch, details.upstreamRef ?? "", - details.defaultBranch ?? "", details.localBranchExists === false ? "0" : "1", details.remoteName ?? "", String(prLookupEpoch(cwd)), @@ -1066,18 +1064,11 @@ export const make = Effect.gen(function* () { }; const prLookupCache = yield* Cache.makeWith( (key: string) => { - const [ - cwd = "", - branch = "", - upstreamRef = "", - defaultBranch = "", - branchExists = "1", - remoteName = "", - ] = key.split("\u0000"); + const [cwd = "", branch = "", upstreamRef = "", branchExists = "1", remoteName = ""] = + key.split("\u0000"); const details = { branch, upstreamRef: upstreamRef.length > 0 ? upstreamRef : null, - defaultBranch: defaultBranch.length > 0 ? defaultBranch : null, localBranchExists: branchExists !== "0", ...(remoteName.length > 0 ? { remoteName } : {}), }; @@ -1173,7 +1164,6 @@ export const make = Effect.gen(function* () { details: { branch: string; upstreamRef: string | null; - defaultBranch: string | null; isDefaultBranch: boolean; }, refreshMissingPullRequest = false, @@ -1261,7 +1251,6 @@ export const make = Effect.gen(function* () { { branch: details.branch, upstreamRef: details.upstreamRef, - defaultBranch: details.defaultBranch, isDefaultBranch: details.isDefaultBranch, }, options?.refreshMissingPullRequest, @@ -1489,10 +1478,9 @@ export const make = Effect.gen(function* () { }).pipe(Effect.orElseSucceed(() => null)); }); - // `git worktree add -b feature origin/main` makes the new local branch track - // origin/main. That upstream is the branch's base, not its published PR - // head. Looking up PRs for it can attach an old reverse merge from main and - // auto-settle an unrelated feature thread. + // A branch cut from origin/main or origin/dev tracks its base, not its + // published PR head. Looking up the base's PR can auto-settle an unrelated + // feature thread. // // The branch may still have been pushed under its own name by a plain // `git push feature` that never moved the upstream. When a remote @@ -1506,20 +1494,20 @@ export const make = Effect.gen(function* () { details: { branch: string; upstreamRef: string | null; - defaultBranch: string | null; remoteName?: string; }, ) { const headContext = yield* resolveBranchHeadContext(cwd, details); - const upstreamHeadIsDefault = - headContext.headBranch === details.defaultBranch || - (details.defaultBranch === null && - (headContext.headBranch === "main" || headContext.headBranch === "master")); - if ( - headContext.headBranch === details.branch || - !upstreamHeadIsDefault || - headContext.isCrossRepository - ) { + // A slash-containing remote can produce a local tracking alias such as + // upstream/effect-atom for my-org/upstream/effect-atom. Both suffix checks + // distinguish that alias from v2 tracking origin/release/v2 and + // feature/dev tracking origin/dev. + const localBranchIsAliasOfHead = + details.branch === headContext.headBranch || + (details.branch.endsWith(`/${headContext.headBranch}`) && + details.upstreamRef !== null && + details.upstreamRef.endsWith(`/${details.branch}`)); + if (localBranchIsAliasOfHead || headContext.isCrossRepository) { return { headContext, lookup: true }; } const remoteName = yield* findRemoteTrackingRemote(cwd, details.branch, headContext.remoteName); @@ -1694,11 +1682,13 @@ export const make = Effect.gen(function* () { finalBranchContext?.hasUpstream === true; if (shouldLookupExistingOpenPr && finalBranchContext) { - latestOpenPr = yield* resolveBranchHeadContext(cwd, { + latestOpenPr = yield* resolveLookupHeadContext(cwd, { branch: finalBranchContext.branch, upstreamRef: finalBranchContext.upstreamRef, }).pipe( - Effect.flatMap((headContext) => findOpenPr(cwd, headContext)), + Effect.flatMap(({ headContext, lookup }) => + lookup ? findOpenPr(cwd, headContext) : Effect.succeed(null), + ), Effect.orElseSucceed(() => null), ); } @@ -2185,7 +2175,6 @@ export const make = Effect.gen(function* () { const cacheKey = prLookupCacheKey(cacheCwd, { branch, upstreamRef, - defaultBranch, localBranchExists, ...(localBranchExists ? {} : { remoteName }), });