authorgravatar for julius0216@outlook.comJulius Marminge <julius0216@outlook.com> 2026-02-11 15:48:41-08:00
committergravatar for julius0216@outlook.comJulius Marminge <julius0216@outlook.com> 2026-02-11 15:48:41-08:00
log94420192359d68c03241f03f47cf8d670abafa15
tree92c5841054415f1d8a25830cab2d3ac21e30ff80
parentcf66f53a5559be6f0117118e07793acf35ea170b

Filter out stale worktree paths from branch listing

Worktrees in /tmp or other ephemeral locations get cleaned by the OS but remain in git's worktree list as "prunable". These stale entries caused branches to show a worktree label pointing at a non-existent directory, leading to broken state when selected. Now validates each worktree path with existsSync before including it in the branch→worktreePath map. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

1 files changed, 3 insertions(+), 0 deletions(-)

apps/server/src/git.ts+3
...@@ -1,4 +1,5 @@...@@ -1,4 +1,5 @@
1import { spawn } from "node:child_process";1import { spawn } from "node:child_process";
2import fs from "node:fs";
2import os from "node:os";3import os from "node:os";
3import path from "node:path";4import path from "node:path";
45
...@@ -131,12 +132,14 @@ export async function listGitBranches(input: GitListBranchesInput): Promise<GitL...@@ -131,12 +132,14 @@ export async function listGitBranches(input: GitListBranchesInput): Promise<GitL
131 defaultRef.code === 0 ? defaultRef.stdout.trim().replace(/^refs\/remotes\/origin\//, "") : null;132 defaultRef.code === 0 ? defaultRef.stdout.trim().replace(/^refs\/remotes\/origin\//, "") : null;
132133
133 // Build branch-name → worktree-path map from porcelain output.134 // Build branch-name → worktree-path map from porcelain output.
135 // Only include worktrees whose directories still exist on disk (skip prunable/stale ones).
134 const worktreeMap = new Map<string, string>();136 const worktreeMap = new Map<string, string>();
135 if (worktreeList.code === 0) {137 if (worktreeList.code === 0) {
136 let currentPath: string | null = null;138 let currentPath: string | null = null;
137 for (const line of worktreeList.stdout.split("\n")) {139 for (const line of worktreeList.stdout.split("\n")) {
138 if (line.startsWith("worktree ")) {140 if (line.startsWith("worktree ")) {
139 currentPath = line.slice("worktree ".length);141 currentPath = line.slice("worktree ".length);
142 if (!fs.existsSync(currentPath)) currentPath = null;
140 } else if (line.startsWith("branch refs/heads/") && currentPath) {143 } else if (line.startsWith("branch refs/heads/") && currentPath) {
141 worktreeMap.set(line.slice("branch refs/heads/".length), currentPath);144 worktreeMap.set(line.slice("branch refs/heads/".length), currentPath);
142 } else if (line === "") {145 } else if (line === "") {