import React from 'react'; import { execCommand } from '@/lib/execCommands'; import type { WorktreeMetadata } from '@/types/worktree'; const normalizePath = (value: string): string => { if (!value) return ''; const replaced = value.replace(/\\/g, '/'); if (replaced === '/') return '/'; return replaced.replace(/\/+$/, ''); }; /** * Derive the primary worktree (project) root from the absolute git directory. * * Secondary worktree: /project/.git/worktrees/ → /project * Primary worktree: /project/.git → null (not a secondary) */ const deriveProjectRoot = (gitDir: string): string | null => { const normalized = normalizePath(gitDir); if (!normalized) return null; const marker = '/.git/worktrees/'; const idx = normalized.indexOf(marker); if (idx > 0) { return normalized.slice(0, idx) || null; } return null; }; /** * When the store-based WorktreeMetadata lookup fails, this hook falls back to * a single `git rev-parse --absolute-git-dir` call to detect whether * `currentDirectory` is a secondary worktree. If it is, a minimal * WorktreeMetadata is synthesised so that "Re-integrate commits" and other * worktree features can function without explicit store entries. * * @param currentDirectory Effective directory for the active session/tab. * @param storeMetadata Result of the normal store-based lookup (may be undefined). * @param currentBranch Current git branch (from status?.current in the parent). */ export function useDetectedWorktreeMetadata( currentDirectory: string | undefined, storeMetadata: WorktreeMetadata | undefined, currentBranch: string | undefined, ): WorktreeMetadata | undefined { const [detected, setDetected] = React.useState(); React.useEffect(() => { if (storeMetadata) { setDetected(undefined); return; } if (!currentDirectory) { setDetected(undefined); return; } // Reset immediately so callers never see stale metadata from a previous directory. setDetected(undefined); let cancelled = false; void (async () => { const [gitDirResult, toplevelResult] = await Promise.all([ execCommand('git rev-parse --absolute-git-dir', currentDirectory), execCommand('git rev-parse --show-toplevel', currentDirectory), ]); if (cancelled) return; if (!gitDirResult.success || !toplevelResult.success) { return; } const gitDir = normalizePath((gitDirResult.stdout || '').trim()); const projectRoot = deriveProjectRoot(gitDir); if (!projectRoot) { return; } // Use the worktree toplevel, not the active sub-directory, so that // worktree operations (e.g. `git worktree remove`) receive a valid root path. const worktreePath = normalizePath((toplevelResult.stdout || '').trim()); // Sanity-check: secondary worktree path must differ from project root if (!worktreePath || worktreePath === projectRoot) { return; } const branch = currentBranch || ''; const name = worktreePath.split('/').filter(Boolean).pop() || worktreePath; const headState = !branch ? 'unborn' : 'branch'; setDetected({ source: 'sdk', path: worktreePath, projectDirectory: projectRoot, branch, label: branch || name, name, // Phase 1 canonical fields — this hook is fallback-only worktreeRoot: worktreePath, worktreeStatus: 'ready', headState, worktreeSource: 'existing', }); })(); return () => { cancelled = true; }; }, [currentDirectory, storeMetadata, currentBranch]); return storeMetadata ?? detected; }