From 6708fe996ac144e606abd8645530ea0d7023d42d Mon Sep 17 00:00:00 2001 From: Julius Marminge Date: Sun, 15 Feb 2026 00:47:31 -0800 Subject: [PATCH] Scope branch refresh fetch to checked-out upstream refspec - derive upstream branch from `branch@{upstream}` and guard empty values - fetch only `+refs/heads/:refs/remotes//` instead of all remote refs - add integration test asserting scoped fetch args during `checkoutBranch` --- apps/server/src/git.test.ts | 49 +++++++++++++++++++++++++++++++++++++ apps/server/src/git.ts | 7 +++++- 2 files changed, 55 insertions(+), 1 deletion(-) diff --git a/apps/server/src/git.test.ts b/apps/server/src/git.test.ts index 075207c6e34bf871b88269470dab77f0b4af097a..323b385edd3c757dc5c268419944e5df12e73e0c 100644 --- a/apps/server/src/git.test.ts +++ b/apps/server/src/git.test.ts @@ -324,6 +324,55 @@ describe("git integration", () => { expect(await git(source.path, "branch --show-current")).toBe(featureBranch); }); + it("refresh fetch is scoped to the checked out branch upstream refspec", async () => { + await using remote = await makeTmpDir(); + await using source = await makeTmpDir(); + await git(remote.path, "init --bare"); + + await initRepoWithCommit(source.path); + const defaultBranch = (await listGitBranches({ cwd: source.path })).branches.find( + (branch) => branch.current, + )!.name; + await git(source.path, `remote add origin ${JSON.stringify(remote.path)}`); + await git(source.path, `push -u origin ${defaultBranch}`); + + const featureBranch = "feature/scoped-fetch"; + await git(source.path, `checkout -b ${featureBranch}`); + await writeFile(path.join(source.path, "feature.txt"), "feature base\n"); + await git(source.path, "add feature.txt"); + await git(source.path, "commit -m 'feature base'"); + await git(source.path, `push -u origin ${featureBranch}`); + await git(source.path, `checkout ${defaultBranch}`); + + class FetchRecordingGitCoreService extends GitCoreService { + fetchArgs: readonly string[] | null = null; + + override async git( + cwd: string, + args: readonly string[], + allowNonZeroExit = false, + ): Promise { + if (args[0] === "fetch") { + this.fetchArgs = args; + return; + } + await super.git(cwd, args, allowNonZeroExit); + } + } + + const core = new FetchRecordingGitCoreService(); + await core.checkoutBranch({ cwd: source.path, branch: featureBranch }); + + expect(await git(source.path, "branch --show-current")).toBe(featureBranch); + expect(core.fetchArgs).toEqual([ + "fetch", + "--quiet", + "--no-tags", + "origin", + `+refs/heads/${featureBranch}:refs/remotes/origin/${featureBranch}`, + ]); + }); + it("throws when branch does not exist", async () => { await using tmp = await makeTmpDir(); await initRepoWithCommit(tmp.path); diff --git a/apps/server/src/git.ts b/apps/server/src/git.ts index 1e310952611c0f24514b73f1e57ed4320eb383fb..ea035d9889a15577877f1aa12f2f96dfd3a99b19 100644 --- a/apps/server/src/git.ts +++ b/apps/server/src/git.ts @@ -570,11 +570,16 @@ export class GitCoreService { return; } const remoteName = upstreamRef.slice(0, separatorIndex); + const upstreamBranch = upstreamRef.slice(separatorIndex + 1); if (remoteName.length === 0) { return; } + if (upstreamBranch.length === 0) { + return; + } - await this.git(cwd, ["fetch", "--quiet", "--no-tags", remoteName], true); + const refspec = `+refs/heads/${upstreamBranch}:refs/remotes/${upstreamRef}`; + await this.git(cwd, ["fetch", "--quiet", "--no-tags", remoteName, refspec], true); } async checkoutBranch(input: GitCheckoutInput): Promise { -- 2.54.0