authorgravatar for julius0216@outlook.comJulius Marminge <julius0216@outlook.com> 2026-02-15 01:14:17-08:00
committergravatar for jmarminge@gmail.comJulius Marminge <jmarminge@gmail.com> 2026-02-15 01:23:10-08:00
log98c4b7c724da7f6aeca67a8c34f53af666fa8a01
tree0097a17f12ea1d25cb65c99bf56a97adb4e78348
parent60111b2b912e8ebc9c21a6bac836b60cf8b75122

fetch in background

Co-authored-by: codex <codex@users.noreply.github.com>

2 files changed, 75 insertions(+), 11 deletions(-)

apps/server/src/git.test.ts+73-6
......@@ -272,11 +272,12 @@ describe("git integration", () => {
272272
273273 await checkoutGitBranch({ cwd: source.path, branch: featureBranch });
274274 const core = new GitCoreService();
275 const details = await core.statusDetails(source.path);
276
277 expect(details.branch).toBe(featureBranch);
278 expect(details.aheadCount).toBe(0);
279 expect(details.behindCount).toBe(1);
275 await vi.waitFor(async () => {
276 const details = await core.statusDetails(source.path);
277 expect(details.branch).toBe(featureBranch);
278 expect(details.aheadCount).toBe(0);
279 expect(details.behindCount).toBe(1);
280 });
280281 });
281282
282283 it("keeps checkout successful when upstream refresh fails", async () => {
......@@ -320,7 +321,9 @@ describe("git integration", () => {
320321 await expect(core.checkoutBranch({ cwd: source.path, branch: featureBranch })).resolves.toBe(
321322 undefined,
322323 );
323 expect(core.refreshFetchAttempts).toBe(1);
324 await vi.waitFor(() => {
325 expect(core.refreshFetchAttempts).toBe(1);
326 });
324327 expect(await git(source.path, "branch --show-current")).toBe(featureBranch);
325328 });
326329
......@@ -362,6 +365,9 @@ describe("git integration", () => {
362365
363366 const core = new FetchRecordingGitCoreService();
364367 await core.checkoutBranch({ cwd: source.path, branch: featureBranch });
368 await vi.waitFor(() => {
369 expect(core.fetchArgs).not.toBeNull();
370 });
365371
366372 expect(await git(source.path, "branch --show-current")).toBe(featureBranch);
367373 expect(core.fetchArgs).toEqual([
......@@ -373,6 +379,67 @@ describe("git integration", () => {
373379 ]);
374380 });
375381
382 it("returns checkout result before background upstream refresh completes", async () => {
383 await using remote = await makeTmpDir();
384 await using source = await makeTmpDir();
385 await git(remote.path, "init --bare");
386
387 await initRepoWithCommit(source.path);
388 const defaultBranch = (await listGitBranches({ cwd: source.path })).branches.find(
389 (branch) => branch.current,
390 )!.name;
391 await git(source.path, `remote add origin ${JSON.stringify(remote.path)}`);
392 await git(source.path, `push -u origin ${defaultBranch}`);
393
394 const featureBranch = "feature/background-refresh";
395 await git(source.path, `checkout -b ${featureBranch}`);
396 await writeFile(path.join(source.path, "feature.txt"), "feature base\n");
397 await git(source.path, "add feature.txt");
398 await git(source.path, "commit -m 'feature base'");
399 await git(source.path, `push -u origin ${featureBranch}`);
400 await git(source.path, `checkout ${defaultBranch}`);
401
402 class SlowRefreshGitCoreService extends GitCoreService {
403 fetchStarted = false;
404 private readonly waitForReleasePromise: Promise<void>;
405 private releaseWait: () => void = () => undefined;
406
407 constructor() {
408 super();
409 this.waitForReleasePromise = new Promise<void>((resolve) => {
410 this.releaseWait = resolve;
411 });
412 }
413
414 releaseFetch(): void {
415 this.releaseWait();
416 }
417
418 override async git(
419 cwd: string,
420 args: readonly string[],
421 allowNonZeroExit = false,
422 ): Promise<void> {
423 if (args[0] === "fetch") {
424 this.fetchStarted = true;
425 await this.waitForReleasePromise;
426 return;
427 }
428 await super.git(cwd, args, allowNonZeroExit);
429 }
430 }
431
432 const core = new SlowRefreshGitCoreService();
433 await expect(core.checkoutBranch({ cwd: source.path, branch: featureBranch })).resolves.toBe(
434 undefined,
435 );
436 await vi.waitFor(() => {
437 expect(core.fetchStarted).toBe(true);
438 });
439 expect(await git(source.path, "branch --show-current")).toBe(featureBranch);
440 core.releaseFetch();
441 });
442
376443 it("throws when branch does not exist", async () => {
377444 await using tmp = await makeTmpDir();
378445 await initRepoWithCommit(tmp.path);
apps/server/src/git.ts+2-5
......@@ -587,11 +587,8 @@ export class GitCoreService {
587587 timeoutMs: 10_000,
588588 fallbackErrorMessage: "git checkout failed",
589589 });
590 try {
591 await this.refreshCheckedOutBranchUpstream(input.cwd);
592 } catch {
593 // Best effort: checkout already succeeded, so avoid surfacing refresh failures.
594 }
590 // Refresh upstream refs in the background so checkout remains responsive.
591 void this.refreshCheckedOutBranchUpstream(input.cwd).catch(() => undefined);
595592 }
596593
597594 async initRepo(input: GitInitInput): Promise<void> {