- 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
Unified · Split
| @@ -1295,5 +1295,54 @@ it.layer(TestLayer)("git integration", (it) => { · 1294 unmodified lines | ||
| 1295 | 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 | }); |
| @@ -664,6 +664,38 @@ const makeGitCore = Effect.gen(function* () { · 663 unmodified lines | ||
| 664 | 664 | ); |
| 665 | 665 | } |
| 666 | 666 | |
| 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 | 699 | const [defaultRef, worktreeList, remoteBranchResult, remoteNamesResult, branchLastCommit] = |
| 668 | 700 | yield* Effect.all( |
| 669 | 701 | [ |
| @@ -685,25 +717,24 @@ const makeGitCore = Effect.gen(function* () { · 15 unmodified lines | ||
| 685 | 717 | allowNonZeroExit: true, |
| 686 | 718 | }, |
| 687 | 719 | ), |
| 688 | − executeGit( | |
| 689 | − "GitCore.listBranches.remoteBranches", | |
| 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 | − }), | |
| 720 | + remoteBranchResultEffect, | |
| 721 | + remoteNamesResultEffect, | |
| 701 | 722 | branchRecencyPromise, |
| 702 | 723 | ], |
| 703 | 724 | { concurrency: "unbounded" }, |
| 704 | 725 | ); |
| 705 | 726 | |
| 706 | 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 | + } | |
| 707 | 738 | |
| 708 | 739 | const defaultBranch = |
| 709 | 740 | defaultRef.code === 0 |