fix(workspaces): hide worktrees whose checkout directory is gone

git worktree list keeps reporting a linked worktree after its directory is deleted
outside PI WEB, marking it prunable. The workspace list showed those as normal
selectable workspaces. The porcelain parser now reads the prunable and locked keys,
and WorkspaceService filters prunable linked worktrees out while always keeping the
project's own path. Listing stays read-only; git worktree prune is never run.
This commit is contained in:
Federico Jaramillo Martinez
2026-07-26 22:25:59 +02:00
parent 8a3fe0daa6
commit 4eca5fe6f2
4 changed files with 177 additions and 3 deletions
@@ -9,6 +9,10 @@ export interface GitWorktreeInfo {
branch?: string;
bare?: boolean;
detached?: boolean;
/** Git reports a linked worktree as prunable when its checkout directory no longer exists. */
prunable?: boolean;
/** Git reports a locked worktree with a bare `locked` line, optionally followed by a reason. */
locked?: boolean;
}
export async function isGitRepository(path: string): Promise<boolean> {
@@ -22,6 +26,14 @@ export async function isGitRepository(path: string): Promise<boolean> {
export async function discoverGitWorktrees(path: string): Promise<GitWorktreeInfo[]> {
const { stdout } = await execFileAsync("git", ["-C", path, "worktree", "list", "--porcelain"], { env: sanitizedGitEnv() });
return parseGitWorktreeList(stdout);
}
/**
* Parses `git worktree list --porcelain` output into facts only. Deciding which worktrees a
* project should show (for example hiding prunable ones) is workspace policy, not parsing.
*/
export function parseGitWorktreeList(stdout: string): GitWorktreeInfo[] {
const chunks = stdout.trim().split(/\n\s*\n/).filter(Boolean);
return chunks.map((chunk) => {
@@ -33,6 +45,8 @@ export async function discoverGitWorktrees(path: string): Promise<GitWorktreeInf
if (key === "branch") info.branch = value.replace(/^refs\/heads\//, "");
if (key === "bare") info.bare = true;
if (key === "detached") info.detached = true;
if (key === "prunable") info.prunable = true;
if (key === "locked") info.locked = true;
}
return info;
}).filter((w) => w.path);