authorgravatar for julius0216@outlook.comJulius Marminge <julius0216@outlook.com> 2026-02-15 00:47:31-08:00
committergravatar for jmarminge@gmail.comJulius Marminge <jmarminge@gmail.com> 2026-02-15 00:53:20-08:00
log6708fe996ac144e606abd8645530ea0d7023d42d
treeafd0d4173be62a5f2f451bbd22c169bd56d9126f
parent72d7fccc525e0d19dfe975221d086a1b816d2045

Scope branch refresh fetch to checked-out upstream refspec

- derive upstream branch from `branch@{upstream}` and guard empty values - fetch only `+refs/heads/<branch>:refs/remotes/<remote>/<branch>` instead of all remote refs - add integration test asserting scoped fetch args during `checkoutBranch`

2 files changed, 55 insertions(+), 1 deletions(-)

apps/server/src/git.test.ts+49
......@@ -324,6 +324,55 @@ describe("git integration", () => {
324324 expect(await git(source.path, "branch --show-current")).toBe(featureBranch);
325325 });
326326
327 it("refresh fetch is scoped to the checked out branch upstream refspec", async () => {
328 await using remote = await makeTmpDir();
329 await using source = await makeTmpDir();
330 await git(remote.path, "init --bare");
331
332 await initRepoWithCommit(source.path);
333 const defaultBranch = (await listGitBranches({ cwd: source.path })).branches.find(
334 (branch) => branch.current,
335 )!.name;
336 await git(source.path, `remote add origin ${JSON.stringify(remote.path)}`);
337 await git(source.path, `push -u origin ${defaultBranch}`);
338
339 const featureBranch = "feature/scoped-fetch";
340 await git(source.path, `checkout -b ${featureBranch}`);
341 await writeFile(path.join(source.path, "feature.txt"), "feature base\n");
342 await git(source.path, "add feature.txt");
343 await git(source.path, "commit -m 'feature base'");
344 await git(source.path, `push -u origin ${featureBranch}`);
345 await git(source.path, `checkout ${defaultBranch}`);
346
347 class FetchRecordingGitCoreService extends GitCoreService {
348 fetchArgs: readonly string[] | null = null;
349
350 override async git(
351 cwd: string,
352 args: readonly string[],
353 allowNonZeroExit = false,
354 ): Promise<void> {
355 if (args[0] === "fetch") {
356 this.fetchArgs = args;
357 return;
358 }
359 await super.git(cwd, args, allowNonZeroExit);
360 }
361 }
362
363 const core = new FetchRecordingGitCoreService();
364 await core.checkoutBranch({ cwd: source.path, branch: featureBranch });
365
366 expect(await git(source.path, "branch --show-current")).toBe(featureBranch);
367 expect(core.fetchArgs).toEqual([
368 "fetch",
369 "--quiet",
370 "--no-tags",
371 "origin",
372 `+refs/heads/${featureBranch}:refs/remotes/origin/${featureBranch}`,
373 ]);
374 });
375
327376 it("throws when branch does not exist", async () => {
328377 await using tmp = await makeTmpDir();
329378 await initRepoWithCommit(tmp.path);
apps/server/src/git.ts+6-1
......@@ -570,11 +570,16 @@ export class GitCoreService {
570570 return;
571571 }
572572 const remoteName = upstreamRef.slice(0, separatorIndex);
573 const upstreamBranch = upstreamRef.slice(separatorIndex + 1);
573574 if (remoteName.length === 0) {
574575 return;
575576 }
577 if (upstreamBranch.length === 0) {
578 return;
579 }
576580
577 await this.git(cwd, ["fetch", "--quiet", "--no-tags", remoteName], true);
581 const refspec = `+refs/heads/${upstreamBranch}:refs/remotes/${upstreamRef}`;
582 await this.git(cwd, ["fetch", "--quiet", "--no-tags", remoteName, refspec], true);
578583 }
579584
580585 async checkoutBranch(input: GitCheckoutInput): Promise<void> {