Skip to content

Commit 5cbf382

Browse files
committed
fix(tui): release the branch watch if the label paint throws
Clear the in-flight guard before onBranch. A throw from the paint callback used to skip the release and the armed follow-up, so later refreshes never looked the branch up again.
1 parent f97a424 commit 5cbf382

2 files changed

Lines changed: 62 additions & 7 deletions

File tree

‎src/tui/workspace-watch.test.ts‎

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,53 @@ describe("watchGitBranch", () => {
5353
watch.stop();
5454
});
5555

56+
test("a throwing onBranch does not stick the guard, so a later refresh looks up again", async () => {
57+
let calls = 0;
58+
const pending: ((branch: string | null) => void)[] = [];
59+
const watch = watchGitBranch({
60+
cwd: "/repo",
61+
onBranch: () => {
62+
throw new Error("paint failed");
63+
},
64+
fetchBranch: () => {
65+
calls += 1;
66+
return new Promise((resolve) => {
67+
pending.push(resolve);
68+
});
69+
},
70+
});
71+
pending[0]?.("main");
72+
await flush();
73+
expect(calls).toBe(1);
74+
75+
watch.refresh();
76+
expect(calls).toBe(2);
77+
watch.stop();
78+
});
79+
80+
test("a follow-up armed before onBranch throws still runs", async () => {
81+
let calls = 0;
82+
const pending: ((branch: string | null) => void)[] = [];
83+
const watch = watchGitBranch({
84+
cwd: "/repo",
85+
onBranch: () => {
86+
throw new Error("paint failed");
87+
},
88+
fetchBranch: () => {
89+
calls += 1;
90+
return new Promise((resolve) => {
91+
pending.push(resolve);
92+
});
93+
},
94+
});
95+
watch.refresh();
96+
expect(calls).toBe(1);
97+
pending[0]?.("main");
98+
await flush();
99+
expect(calls).toBe(2);
100+
watch.stop();
101+
});
102+
56103
test("a null result after a known branch is not reported", async () => {
57104
const seen: (string | null)[] = [];
58105
let branch: string | null = "main";

‎src/tui/workspace-watch.ts‎

Lines changed: 15 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -50,14 +50,22 @@ export function watchGitBranch(input: BranchWatchInput): BranchWatch {
5050
again = false;
5151
fetchBranch(input.cwd).then(
5252
(branch) => {
53-
// Null after a real branch is a failed lookup (or detached HEAD, which
54-
// shares that null). Keep the suffix; do not clear it.
55-
if (!stopped && !(branch === null && shown !== null)) {
56-
shown = branch;
57-
input.onBranch(branch);
58-
}
53+
// Release before paint. A throw from onBranch must not stick the
54+
// guard or skip a follow-up armed while this lookup was in flight,
55+
// and it must not reject this chain.
5956
inFlight = false;
60-
if (!stopped && again) refresh();
57+
try {
58+
// Null after a real branch is a failed lookup (or detached HEAD,
59+
// which shares that null). Keep the suffix; do not clear it.
60+
if (!stopped && !(branch === null && shown !== null)) {
61+
input.onBranch(branch);
62+
shown = branch;
63+
}
64+
} catch {
65+
// The label paint must not stick the lookup or reject this chain.
66+
} finally {
67+
if (!stopped && again) refresh();
68+
}
6169
},
6270
() => {
6371
inFlight = false;

0 commit comments

Comments
 (0)