/** * Git directory resolution — where a project's git metadata actually lives. * * The Git panel is driven by filesystem events on git's own metadata, so the * watcher has to know every directory that metadata lives in. `/.git` * is only the common case: * * - a project opened at a SUBFOLDER of a repo has no `.git` of its own; the * real one sits further up, outside the watched tree * - a linked worktree's `.git` is a FILE pointing at * `
/.git/worktrees/`, while `refs/` stays in the shared common * dir — so HEAD and the branch list are watched in two different places * - a submodule's `.git` is a FILE pointing into the superproject * - nested sub-repos inside the project each have their own git dir * * `git rev-parse` answers the first three authoritatively; `findNestedRepoPaths` * (the SSOT for "what counts as a sub-repo") answers the last. Probing for a * `.git` directory by hand — which is what the watcher used to do — silently * finds nothing in every case above and leaves the panel with no event source * at all. */ import path from 'node:path'; import { debug } from '$shared/utils/logger'; import { execGit } from './git-executor'; import { findNestedRepoPaths } from './nested-repos'; export interface GitDirTarget { /** * Absolute path of the working tree these git files belong to — the value * handed back to clients so a change can be attributed to the right repo. * This is the path we were asked about (the project root, or a sub-repo * root), NOT necessarily the repository's top level: a project opened at a * repo subfolder keeps identifying itself by that subfolder. */ repoPath: string; /** Absolute path of the git directory (per-worktree for linked worktrees). */ gitDir: string; /** Absolute path of the shared git directory — where `refs/` really lives. */ commonDir: string; } /** * Resolve one working tree's git directories, or null when it is not a repo. */ async function resolveGitDirTarget(repoPath: string): Promise { try { const result = await execGit(['rev-parse', '--git-dir', '--git-common-dir'], repoPath, 5000); if (result.exitCode !== 0) return null; const lines = result.stdout.trim().split('\n'); const gitDirRaw = lines[0]?.trim(); if (!gitDirRaw) return null; const commonDirRaw = lines[1]?.trim(); // `rev-parse` answers relative to its cwd when the dir is inside it. const gitDir = path.resolve(repoPath, gitDirRaw); const commonDir = commonDirRaw ? path.resolve(repoPath, commonDirRaw) : gitDir; return { repoPath, gitDir, commonDir }; } catch (error) { debug.warn('git', `Failed to resolve git dir for ${repoPath}:`, error); return null; } } /** * Every git directory whose contents should make the project's Git panel * refresh: the project's own repo plus each nested sub-repo. */ export async function resolveGitDirs(projectPath: string): Promise { const targets: GitDirTarget[] = []; const seen = new Set(); const addTarget = (target: GitDirTarget | null): void => { if (!target) return; const key = `${target.repoPath}${target.gitDir}`; if (seen.has(key)) return; seen.add(key); targets.push(target); }; addTarget(await resolveGitDirTarget(projectPath)); try { const nestedRepoPaths = await findNestedRepoPaths(projectPath); for (const nestedRepoPath of nestedRepoPaths) { addTarget(await resolveGitDirTarget(nestedRepoPath)); } } catch (error) { // A failure here only costs us sub-repo events; the outer repo still works. debug.warn('git', `Failed to resolve nested git dirs for ${projectPath}:`, error); } return targets; }