diff --git a/apps/server/src/git.test.ts b/apps/server/src/git.test.ts index 9155cb04e2301f9e1078c90a90a1f5f4cd39582d..1d9067b7aac3a9198fa29f4715a20a4727637bc2 100644 --- a/apps/server/src/git.test.ts +++ b/apps/server/src/git.test.ts @@ -2,7 +2,7 @@ import { existsSync } from "node:fs"; import { mkdtemp, rm, writeFile } from "node:fs/promises"; import { tmpdir } from "node:os"; import path from "node:path"; -import { describe, expect, it } from "vitest"; +import { describe, expect, it, vi } from "vitest"; import { GitCoreService, @@ -603,5 +603,21 @@ describe("git integration", () => { const skipped = await core.pushCurrentBranch(tmp.path, null); expect(skipped.status).toBe("skipped_up_to_date"); }); + + it("lists branches when recency lookup fails", async () => { + await using tmp = await makeTmpDir(); + await initRepoWithCommit(tmp.path); + const core = new GitCoreService(); + const recencySpy = vi + .spyOn(core as any, "readBranchRecency") + .mockRejectedValueOnce(new Error("timeout")); + + const result = await core.listBranches({ cwd: tmp.path }); + + expect(result.isRepo).toBe(true); + expect(result.branches.length).toBeGreaterThan(0); + expect(result.branches[0]?.current).toBe(true); + recencySpy.mockRestore(); + }); }); }); diff --git a/apps/server/src/git.ts b/apps/server/src/git.ts index 39e4e1f93d9ad01bcbd201018fa49fff1d4f349a..b6fdbe63a84a86791e5cf0198506825868cf9cde 100644 --- a/apps/server/src/git.ts +++ b/apps/server/src/git.ts @@ -312,7 +312,40 @@ export class GitCoreService { return value.length > 0 ? value : null; } + private async readBranchRecency(cwd: string): Promise> { + const branchRecency = await executeGit( + cwd, + ["for-each-ref", "--format=%(refname:short)%09%(committerdate:unix)", "refs/heads"], + { + timeoutMs: 15_000, + allowNonZeroExit: true, + }, + ); + + const branchLastCommit = new Map(); + if (branchRecency.code !== 0) { + return branchLastCommit; + } + + for (const line of branchRecency.stdout.split("\n")) { + if (line.length === 0) { + continue; + } + const [name, lastCommitRaw] = line.split("\t"); + if (!name) { + continue; + } + const lastCommit = Number.parseInt(lastCommitRaw ?? "0", 10); + branchLastCommit.set(name, Number.isFinite(lastCommit) ? lastCommit : 0); + } + + return branchLastCommit; + } + async listBranches(input: GitListBranchesInput): Promise { + const branchRecencyPromise = this.readBranchRecency(input.cwd).catch( + () => new Map(), + ); const result = await executeGit(input.cwd, ["branch", "--no-color"], { timeoutMs: 10_000, allowNonZeroExit: true, @@ -326,7 +359,7 @@ export class GitCoreService { throw new Error(stderr || "git branch failed"); } - const [defaultRef, worktreeList, branchRecency] = await Promise.all([ + const [defaultRef, worktreeList, branchLastCommit] = await Promise.all([ executeGit(input.cwd, ["symbolic-ref", "refs/remotes/origin/HEAD"], { timeoutMs: 5_000, allowNonZeroExit: true, @@ -335,14 +368,7 @@ export class GitCoreService { timeoutMs: 5_000, allowNonZeroExit: true, }), - executeGit( - input.cwd, - ["for-each-ref", "--format=%(refname:short)%09%(committerdate:unix)", "refs/heads"], - { - timeoutMs: 5_000, - allowNonZeroExit: true, - }, - ), + branchRecencyPromise, ]); const defaultBranch = defaultRef.code === 0 @@ -364,21 +390,6 @@ export class GitCoreService { } } - const branchLastCommit = new Map(); - if (branchRecency.code === 0) { - for (const line of branchRecency.stdout.split("\n")) { - if (line.length === 0) { - continue; - } - const [name, lastCommitRaw] = line.split("\t"); - if (!name) { - continue; - } - const lastCommit = Number.parseInt(lastCommitRaw ?? "0", 10); - branchLastCommit.set(name, Number.isFinite(lastCommit) ? lastCommit : 0); - } - } - const branches = result.stdout .split("\n") .map((line) => line.trim())