diff --git a/apps/server/src/vcs/GitVcsDriverCore.test.ts b/apps/server/src/vcs/GitVcsDriverCore.test.ts index 5b84236db14c0..070ebed246c09 100644 --- a/apps/server/src/vcs/GitVcsDriverCore.test.ts +++ b/apps/server/src/vcs/GitVcsDriverCore.test.ts @@ -7007,6 +7007,27 @@ it.layer(layerTest)("GitVcsDriver core integration", (it) => { }), ); + it.effect("reads a recorded branch base as git config --get would", () => + Effect.gen(function* () { + const cwd = yield* makeTmpDir(); + yield* initRepoWithCommit(cwd); + const driver = yield* GitVcsDriver.GitVcsDriver; + const branch = "feature/a.b+"; + yield* git(cwd, ["config", "branch.feature/aXbb.t3-merge-base", "regex-match"]); + assert.isNull(yield* driver.readBranchBase(cwd, branch)); + + yield* git(cwd, ["config", `branch.${branch}.gh-merge-base`, "legacy"]); + assert.equal(yield* driver.readBranchBase(cwd, branch), "legacy"); + + yield* git(cwd, ["config", `branch.${branch}.t3-merge-base`, "first"]); + yield* git(cwd, ["config", "--add", `branch.${branch}.t3-merge-base`, "last"]); + assert.equal(yield* driver.readBranchBase(cwd, branch), "last"); + + yield* git(cwd, ["config", "--add", `branch.${branch}.t3-merge-base`, ""]); + assert.equal(yield* driver.readBranchBase(cwd, branch), "legacy"); + }), + ); + it.effect.each([ { upstreamRef: "refs/remotes/origin/main", baseKey: "gh-merge-base" }, { upstreamRef: "refs/heads/main", baseKey: "gh-merge-base" }, diff --git a/apps/server/src/vcs/GitVcsDriverCore.ts b/apps/server/src/vcs/GitVcsDriverCore.ts index 12ce999f25e18..e7656ec8784ea 100644 --- a/apps/server/src/vcs/GitVcsDriverCore.ts +++ b/apps/server/src/vcs/GitVcsDriverCore.ts @@ -59,10 +59,13 @@ const DEFAULT_TIMEOUT_MS = 30_000; /** T3's record of the branch, named as on the host, that a branch was cut from. */ const branchBaseConfigKey = (branch: string) => `branch.${branch}.t3-merge-base`; /** - * Where T3 kept that record before it had a key of its own, for every host alike: the GitHub - * CLI's key, which branches cut before then still carry instead. + * T3's keys plus the GitHub CLI's `gh-merge-base`, where T3 kept the record for every host alike + * before it had a key of its own; branches cut before then still carry it instead. */ -const legacyBranchBaseConfigKey = (branch: string) => `branch.${branch}.gh-merge-base`; +const branchBaseConfigPattern = (branch: string) => { + const escapedBranch = branch.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"); + return `^branch\\.${escapedBranch}\\.(t3-merge-base-ref|t3-merge-base|gh-merge-base)$`; +}; const gitProcesses = Semaphore.makeUnsafe(8); const backgroundGitProcesses = Semaphore.makeUnsafe(2); const WORKTREE_INDEX_REFRESH_TIMEOUT_MS = Duration.toMillis(Duration.minutes(5)); @@ -1908,7 +1911,10 @@ export const makeGitVcsDriverCore = Effect.fn("makeGitVcsDriverCore")(function* readonly knownOriginDefaultBranch?: string | null; }, ) { - const configuredBaseRef = yield* readConfigValue(cwd, `branch.${refName}.t3-merge-base-ref`); + const { baseRef: configuredBaseRef, base: recordedBase } = yield* readBranchBaseConfig( + cwd, + refName, + ); if ( configuredBaseRef?.startsWith("refs/heads/") || configuredBaseRef?.startsWith("refs/remotes/") @@ -1923,7 +1929,7 @@ export const makeGitVcsDriverCore = Effect.fn("makeGitVcsDriverCore")(function* } } - const configuredBaseBranch = (yield* readBranchBase(cwd, refName)) ?? ""; + const configuredBaseBranch = recordedBase ?? ""; const configuredBase = yield* resolveComparisonBaseRef(cwd, configuredBaseBranch); if ( configuredBase !== null && @@ -4010,11 +4016,35 @@ export const makeGitVcsDriverCore = Effect.fn("makeGitVcsDriverCore")(function* yield* createBranchAt("GitVcsDriver.createBranch", input, "git branch create failed"); }); + // The last value of a repeated key wins, as with `git config --get`. + const readBranchBaseConfig = Effect.fn("GitVcsDriver.readBranchBaseConfig")(function* ( + cwd: string, + branch: string, + ) { + const stdout = yield* runGitStdout( + "GitVcsDriver.readBranchBaseConfig", + cwd, + ["config", "-z", "--get-regexp", branchBaseConfigPattern(branch)], + true, + ); + const values = new Map(); + for (const entry of stdout.split("\0")) { + if (entry.length === 0) continue; + const newline = entry.indexOf("\n"); + const key = newline < 0 ? entry : entry.slice(0, newline); + const value = newline < 0 ? "" : entry.slice(newline + 1).trim(); + values.set(key.slice(key.lastIndexOf(".") + 1), value.length > 0 ? value : null); + } + return { + baseRef: values.get("t3-merge-base-ref") ?? null, + base: values.get("t3-merge-base") ?? values.get("gh-merge-base") ?? null, + }; + }); + const readBranchBase: GitVcsDriver.GitVcsDriver["Service"]["readBranchBase"] = Effect.fn( "GitVcsDriver.readBranchBase", )(function* (cwd, branch) { - const base = yield* readConfigValue(cwd, branchBaseConfigKey(branch)); - return base ?? (yield* readConfigValue(cwd, legacyBranchBaseConfigKey(branch))); + return (yield* readBranchBaseConfig(cwd, branch)).base; }); const resolveBranchBase = Effect.fn("GitVcsDriver.resolveBranchBase")(function* (