authorgravatar for julius0216@outlook.comJulius Marminge <julius0216@outlook.com> 2026-02-12 23:28:10-08:00
committergravatar for jmarminge@gmail.comJulius Marminge <jmarminge@gmail.com> 2026-02-12 23:29:44-08:00
log6b916d3a6fbe2145ac53776c73ba7b3d0aa969de
treea4ff8856f92390614a05277ffdd0cfa50879d6e0
parent21580dc1b0860e5cc711ab2cc494b52e64103d92

Handle branch recency lookup failures in branch listing

- move branch recency parsing into `readBranchRecency` - fall back to an empty recency map if lookup fails - add test to ensure branches still list when recency read times out Co-authored-by: codex <codex@users.noreply.github.com>

2 files changed, 52 insertions(+), 25 deletions(-)

apps/server/src/git.test.ts+17-1
......@@ -2,7 +2,7 @@ import { existsSync } from "node:fs";
22import { mkdtemp, rm, writeFile } from "node:fs/promises";
33import { tmpdir } from "node:os";
44import path from "node:path";
5import { describe, expect, it } from "vitest";
5import { describe, expect, it, vi } from "vitest";
66
77import {
88 GitCoreService,
......@@ -603,5 +603,21 @@ describe("git integration", () => {
603603 const skipped = await core.pushCurrentBranch(tmp.path, null);
604604 expect(skipped.status).toBe("skipped_up_to_date");
605605 });
606
607 it("lists branches when recency lookup fails", async () => {
608 await using tmp = await makeTmpDir();
609 await initRepoWithCommit(tmp.path);
610 const core = new GitCoreService();
611 const recencySpy = vi
612 .spyOn(core as any, "readBranchRecency")
613 .mockRejectedValueOnce(new Error("timeout"));
614
615 const result = await core.listBranches({ cwd: tmp.path });
616
617 expect(result.isRepo).toBe(true);
618 expect(result.branches.length).toBeGreaterThan(0);
619 expect(result.branches[0]?.current).toBe(true);
620 recencySpy.mockRestore();
621 });
606622 });
607623});
apps/server/src/git.ts+35-24
......@@ -312,7 +312,40 @@ export class GitCoreService {
312312 return value.length > 0 ? value : null;
313313 }
314314
315 private async readBranchRecency(cwd: string): Promise<Map<string, number>> {
316 const branchRecency = await executeGit(
317 cwd,
318 ["for-each-ref", "--format=%(refname:short)%09%(committerdate:unix)", "refs/heads"],
319 {
320 timeoutMs: 15_000,
321 allowNonZeroExit: true,
322 },
323 );
324
325 const branchLastCommit = new Map<string, number>();
326 if (branchRecency.code !== 0) {
327 return branchLastCommit;
328 }
329
330 for (const line of branchRecency.stdout.split("\n")) {
331 if (line.length === 0) {
332 continue;
333 }
334 const [name, lastCommitRaw] = line.split("\t");
335 if (!name) {
336 continue;
337 }
338 const lastCommit = Number.parseInt(lastCommitRaw ?? "0", 10);
339 branchLastCommit.set(name, Number.isFinite(lastCommit) ? lastCommit : 0);
340 }
341
342 return branchLastCommit;
343 }
344
315345 async listBranches(input: GitListBranchesInput): Promise<GitListBranchesResult> {
346 const branchRecencyPromise = this.readBranchRecency(input.cwd).catch(
347 () => new Map<string, number>(),
348 );
316349 const result = await executeGit(input.cwd, ["branch", "--no-color"], {
317350 timeoutMs: 10_000,
318351 allowNonZeroExit: true,
......@@ -326,7 +359,7 @@ export class GitCoreService {
326359 throw new Error(stderr || "git branch failed");
327360 }
328361
329 const [defaultRef, worktreeList, branchRecency] = await Promise.all([
362 const [defaultRef, worktreeList, branchLastCommit] = await Promise.all([
330363 executeGit(input.cwd, ["symbolic-ref", "refs/remotes/origin/HEAD"], {
331364 timeoutMs: 5_000,
332365 allowNonZeroExit: true,
......@@ -335,14 +368,7 @@ export class GitCoreService {
335368 timeoutMs: 5_000,
336369 allowNonZeroExit: true,
337370 }),
338 executeGit(
339 input.cwd,
340 ["for-each-ref", "--format=%(refname:short)%09%(committerdate:unix)", "refs/heads"],
341 {
342 timeoutMs: 5_000,
343 allowNonZeroExit: true,
344 },
345 ),
371 branchRecencyPromise,
346372 ]);
347373 const defaultBranch =
348374 defaultRef.code === 0
......@@ -364,21 +390,6 @@ export class GitCoreService {
364390 }
365391 }
366392
367 const branchLastCommit = new Map<string, number>();
368 if (branchRecency.code === 0) {
369 for (const line of branchRecency.stdout.split("\n")) {
370 if (line.length === 0) {
371 continue;
372 }
373 const [name, lastCommitRaw] = line.split("\t");
374 if (!name) {
375 continue;
376 }
377 const lastCommit = Number.parseInt(lastCommitRaw ?? "0", 10);
378 branchLastCommit.set(name, Number.isFinite(lastCommit) ? lastCommit : 0);
379 }
380 }
381
382393 const branches = result.stdout
383394 .split("\n")
384395 .map((line) => line.trim())