Something went wrong. Try again.
Something went wrong. Try again.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101/** * Conservative cwd Git classification (saga j8dtju.5 — Workspace domain, phase 1). * * Classifies a cwd into one of three buckets WITHOUT persisting, binding, or creating * anything — classification is a read-only probe used by callers (the future catalog, the * daemon's legacy-session reconciliation in `.7`) to decide what to do next, never an action. * * Buckets: * - `non-git` — cwd is not inside any Git repository. * - `main-checkout` — cwd is in a repository AND this worktree is the main checkout (the * `.git` directory lives at the repo common-dir; `gitDir === commonDir`). * - `linked-worktree` — cwd is in a repository and this worktree is a LINKED worktree (the * per-worktree git dir lives under `<commonDir>/worktrees/<name>` and * differs from `commonDir`). * * The classifier delegates identity resolution to {@link GitRepositoryResolver} so there is one * code path producing canonical identity. It does NOT consult `git worktree list` for * classification (that is a richer query the catalog uses for enumeration); the common-dir vs * git-dir relationship is sufficient and cheaper. */import type { ResolvedWorkspaceIdentity } from '@/core/git/resolver';import { GitRepositoryResolver, type ResolveIdentityOutcome } from '@/core/git/resolver';import type { GitRunner } from '@/core/git/runner';
/** The discriminant for a classified cwd. */export type CwdClassificationKind = 'non-git' | 'main-checkout' | 'linked-worktree';
/** A read-only classification result. Carries identity only when Git-backed. */export type CwdClassification = | { kind: 'non-git'; message: string } | { kind: 'main-checkout'; identity: ResolvedWorkspaceIdentity } | { kind: 'linked-worktree'; identity: ResolvedWorkspaceIdentity };
/** * Classify `cwd` against Git. Returns the bucket + identity (when Git-backed). Never mutates * Git or persists anything. A `probe-failed` resolution (e.g. a transient git timeout) is * surfaced as `non-git` conservatively — this is classification, not a retry surface; the * caller that needs to distinguish a transient failure should call the resolver directly. */export async function classifyCwd( cwd: string, resolver: GitRepositoryResolver,): Promise<CwdClassification> { const outcome: ResolveIdentityOutcome = await resolver.resolveIdentity(cwd); if (outcome.kind !== 'ok') { return { kind: 'non-git', message: outcome.message }; } return classifyResolved(outcome.identity);}
/** Classify an already-resolved identity (no extra Git calls). Pure. */export function classifyResolved(identity: ResolvedWorkspaceIdentity): CwdClassification { // A main checkout's per-worktree git dir IS the common dir. A linked worktree's git dir is a // subdirectory of commonDir (commonDir/worktrees/<name>), so the two paths differ. Both are // realpath-canonicalized by the resolver, so a string compare is safe across spellings. if (identity.worktree.gitDir === identity.repository.commonDir) { return { kind: 'main-checkout', identity }; } return { kind: 'linked-worktree', identity };}
/** * The canonical main-checkout root of a repository, derived from its canonical common-dir. * * For a normal (non-bare) checkout the common-dir is the repository's `.git` directory * (`<main-checkout>/.git`) — shared by the main checkout and EVERY linked worktree — so the * project root is the path with the trailing `/.git` stripped. For a bare repository the * common-dir IS the repository root and is returned as-is. A common-dir that does not end * with `/.git` (e.g. a submodule's `.git/modules/...` gitdir) falls back to the common-dir * itself: the derivation is a best-effort display projection, never an authority. * * The result is a DISPLAY path for grouping/labeling (e.g. naming a worktree's original * project in the web dashboard or CLI) — it is NOT the common-dir itself and must not be * treated as a git-dir. Pure (string arithmetic only). */export function repositoryRootFromCommonDir(commonDir: string): string { const trimmed = commonDir.replace(/\/+$/, ''); const suffix = '/.git'; if (trimmed.length > suffix.length && trimmed.endsWith(suffix)) { return trimmed.slice(0, trimmed.length - suffix.length); } return trimmed;}
/** * Build a classifier bound to a specific runner. Convenience for callers that already hold a * runner; the classifier is stateless and safe to share. */export function createClassifier(runner: GitRunner): { classify(cwd: string): Promise<CwdClassification>; resolver: GitRepositoryResolver;} { const resolver = new GitRepositoryResolver(runner); return { resolver, async classify(cwd: string): Promise<CwdClassification> { return classifyCwd(cwd, resolver); }, };}