authorgravatar for julius0216@outlook.comJulius Marminge <julius0216@outlook.com> 2026-02-28 21:32:30-08:00
committergravatar for jmarminge@gmail.comJulius Marminge <jmarminge@gmail.com> 2026-02-28 21:56:22-08:00
log16fed12dcc5432978d3bfcd78f2debf1890522b0
tree08c9048283cd86db8f9432d3c56576df105522f5
parent29825f2f8a7cc2bb47fd32160046d47b5cf44830

Handle remote lookup failures in branch listing

- Fall back to empty remote branch and remote name data when git remote queries fail - Log warnings instead of failing `listBranches` - Add integration test covering remote lookup failure fallback

2 files changed, 93 insertions(+), 13 deletions(-)

apps/server/src/git/Layers/GitCore.test.ts+49
...@@ -1295,5 +1295,54 @@ it.layer(TestLayer)("git integration", (it) => {...@@ -1295,5 +1295,54 @@ it.layer(TestLayer)("git integration", (it) => {
1295 expect(didFailRecency).toBe(true);1295 expect(didFailRecency).toBe(true);
1296 }),1296 }),
1297 );1297 );
1298
1299 it.effect("falls back to empty remote branch data when remote lookups fail", () =>
1300 Effect.gen(function* () {
1301 const tmp = yield* makeTmpDir();
1302 const remote = yield* makeTmpDir();
1303 yield* initRepoWithCommit(tmp);
1304 yield* git(remote, ["init", "--bare"]);
1305 yield* git(tmp, ["remote", "add", "origin", remote]);
1306
1307 const realGitService = yield* GitService;
1308 let didFailRemoteBranches = false;
1309 let didFailRemoteNames = false;
1310 const core = yield* makeIsolatedGitCore({
1311 execute: (input) => {
1312 if (input.args.join(" ") === "branch --no-color --remotes") {
1313 didFailRemoteBranches = true;
1314 return Effect.fail(
1315 new GitCommandError({
1316 operation: "git.test.listBranchesRemoteBranches",
1317 command: `git ${input.args.join(" ")}`,
1318 cwd: input.cwd,
1319 detail: "remote unavailable",
1320 }),
1321 );
1322 }
1323 if (input.args.join(" ") === "remote") {
1324 didFailRemoteNames = true;
1325 return Effect.fail(
1326 new GitCommandError({
1327 operation: "git.test.listBranchesRemoteNames",
1328 command: `git ${input.args.join(" ")}`,
1329 cwd: input.cwd,
1330 detail: "remote unavailable",
1331 }),
1332 );
1333 }
1334 return realGitService.execute(input);
1335 },
1336 });
1337
1338 const result = yield* core.listBranches({ cwd: tmp });
1339
1340 expect(result.isRepo).toBe(true);
1341 expect(result.branches.length).toBeGreaterThan(0);
1342 expect(result.branches.every((branch) => !branch.isRemote)).toBe(true);
1343 expect(didFailRemoteBranches).toBe(true);
1344 expect(didFailRemoteNames).toBe(true);
1345 }),
1346 );
1298 });1347 });
1299});1348});
apps/server/src/git/Layers/GitCore.ts+44-13
...@@ -664,6 +664,38 @@ const makeGitCore = Effect.gen(function* () {...@@ -664,6 +664,38 @@ const makeGitCore = Effect.gen(function* () {
664 );664 );
665 }665 }
666666
667 const remoteBranchResultEffect = executeGit(
668 "GitCore.listBranches.remoteBranches",
669 input.cwd,
670 ["branch", "--no-color", "--remotes"],
671 {
672 timeoutMs: 10_000,
673 allowNonZeroExit: true,
674 },
675 ).pipe(
676 Effect.catch((error) =>
677 Effect.logWarning(
678 `GitCore.listBranches: remote branch lookup failed for ${input.cwd}: ${error.message}. Falling back to an empty remote branch list.`,
679 ).pipe(Effect.as({ code: 1, stdout: "", stderr: "" })),
680 ),
681 );
682
683 const remoteNamesResultEffect = executeGit(
684 "GitCore.listBranches.remoteNames",
685 input.cwd,
686 ["remote"],
687 {
688 timeoutMs: 5_000,
689 allowNonZeroExit: true,
690 },
691 ).pipe(
692 Effect.catch((error) =>
693 Effect.logWarning(
694 `GitCore.listBranches: remote name lookup failed for ${input.cwd}: ${error.message}. Falling back to an empty remote name list.`,
695 ).pipe(Effect.as({ code: 1, stdout: "", stderr: "" })),
696 ),
697 );
698
667 const [defaultRef, worktreeList, remoteBranchResult, remoteNamesResult, branchLastCommit] =699 const [defaultRef, worktreeList, remoteBranchResult, remoteNamesResult, branchLastCommit] =
668 yield* Effect.all(700 yield* Effect.all(
669 [701 [
...@@ -685,25 +717,24 @@ const makeGitCore = Effect.gen(function* () {...@@ -685,25 +717,24 @@ const makeGitCore = Effect.gen(function* () {
685 allowNonZeroExit: true,717 allowNonZeroExit: true,
686 },718 },
687 ),719 ),
688 executeGit(720 remoteBranchResultEffect,
689 "GitCore.listBranches.remoteBranches",721 remoteNamesResultEffect,
690 input.cwd,
691 ["branch", "--no-color", "--remotes"],
692 {
693 timeoutMs: 10_000,
694 allowNonZeroExit: true,
695 },
696 ),
697 executeGit("GitCore.listBranches.remoteNames", input.cwd, ["remote"], {
698 timeoutMs: 5_000,
699 allowNonZeroExit: true,
700 }),
701 branchRecencyPromise,722 branchRecencyPromise,
702 ],723 ],
703 { concurrency: "unbounded" },724 { concurrency: "unbounded" },
704 );725 );
705726
706 const remoteNames = remoteNamesResult.code === 0 ? parseRemoteNames(remoteNamesResult.stdout) : [];727 const remoteNames = remoteNamesResult.code === 0 ? parseRemoteNames(remoteNamesResult.stdout) : [];
728 if (remoteBranchResult.code !== 0 && remoteBranchResult.stderr.trim().length > 0) {
729 yield* Effect.logWarning(
730 `GitCore.listBranches: remote branch lookup returned code ${remoteBranchResult.code} for ${input.cwd}: ${remoteBranchResult.stderr.trim()}. Falling back to an empty remote branch list.`,
731 );
732 }
733 if (remoteNamesResult.code !== 0 && remoteNamesResult.stderr.trim().length > 0) {
734 yield* Effect.logWarning(
735 `GitCore.listBranches: remote name lookup returned code ${remoteNamesResult.code} for ${input.cwd}: ${remoteNamesResult.stderr.trim()}. Falling back to an empty remote name list.`,
736 );
737 }
707738
708 const defaultBranch =739 const defaultBranch =
709 defaultRef.code === 0740 defaultRef.code === 0