Files
orca/src/main/git/worktree-shared-directories.ts
T
Neil 77f23b013f refactor(shared): drop the shared/types barrel and import from the real modules (#14447)
#14397 split `shared/types.ts` into 46 per-domain modules but kept the path as
a re-export barrel so the import sites did not have to change. This removes
the barrel: every consumer now imports from the module that actually declares
the type, and `src/shared/types.ts` is deleted.

Barrels hide where a type lives, make every consumer look like it depends on
the whole domain, and let an unrelated edit invalidate a module that ~2,000
files transitively import.

2,323 import declarations across 2,321 files. Rewritten mechanically: each
specifier was resolved to an absolute path via the TypeScript AST and
recomputed, rather than string-substituted, so alias forms (`@/../../shared/
types`) and per-specifier `type` modifiers survive.

Four cases the mechanical pass had to handle, each found by a gate rather than
by reading the diff:

- Modules inside `src/shared` import the barrel as `./types`, not
  `shared/types`. A pre-filter on the latter string skipped 176 of them and
  left imports dangling at a deleted file, which surfaced as confusing
  `Property 'x' is optional in type 'Repo' but required in Pick<Repo, ...>`
  errors rather than "module not found".
- The barrel RENAMED one type on the way through
  (`WorkspaceSource as WorkspaceCreateTelemetrySource`), so the original name
  in the owning module has to be re-aliased at each consumer.
- Three test files put `;(globalThis as ...)` on the line after the import.
  TypeScript parses that `;` as the import statement's terminator, so
  replacing through `statement.getEnd()` deletes it and breaks ASI. The
  rewrite now stops at the module specifier.
- A file that already imported directly from a module got a SECOND import
  from it, because the barrel re-exported those same names — which trips
  `import/no-duplicates` under `--deny-warnings`. A post-pass merges
  declarations sharing a specifier and type-only-ness; the `import type` plus
  `import` pair from one module is left alone, since that form is allowed.

Splitting one barrel import into several genuinely adds lines, which pushed
`terminal-layout-pty-ownership.ts` to 301 counted lines: its 107-character
import must wrap, and neither local type collapses onto one line (101 and 116
characters). Rather than contort a type declaration to fit a line budget,
`collectLeafIds` and `pruneLeaves` move to `terminal-pane-layout-tree.ts` —
they are pure structural operations on the layout tree and independent of PTY
ownership. `visible-worktrees.ts` similarly loses its own mini-barrel
re-export of `isDefaultBranchWorkspace`, with the four real consumers
repointed at the declaring module. No `max-lines` bypass added.

Verified: cold `tsc --noEmit` green on node, cli, and web (buildinfo deleted
first — these projects are `composite: true` and reuse stale caches); the full
`pnpm lint` green, not just bare oxlint — the narrower local check is what let
the duplicate imports reach CI; max-lines ratchet OK at 344.
2026-08-13 22:48:24 -07:00

112 lines
4.5 KiB
TypeScript

import { stat } from 'node:fs/promises'
import { join } from 'node:path'
import { checkIgnoredPaths } from './check-ignored-paths'
import type { GitRuntimeOptions } from './git-runtime-options'
import { loadHooks } from '../hooks'
import type { Repo } from '../../shared/repo-types'
// Why: a fresh worktree has no node_modules/.cache, and copying them is slow and
// duplicates disk; `orca.yaml` names the ones every worktree should share instead.
const CONFIGURED_SHARED_DIRECTORIES_CACHE_TTL_MS = 30_000
const configuredSharedDirectoriesByRepoPath = new Map<
string,
{ directories: string[]; expiresAt: number }
>()
/** The configured `worktree.sharedDirectories` names, before any existence or
* gitignore filtering.
*
* Why deletion can't reuse the resolver below: a directory-only ignore rule
* (`node_modules/`) matches the primary's real directory but never the
* worktree's symlink, so Git reports that symlink as untracked. Removal has to
* tolerate and unlink it, and the resolver would have already dropped it.
*
* `readonly` because this is the cached array itself: a mutating caller would
* corrupt every later read for the rest of the TTL. Copying on return would fix
* that too, but this runs on the status-polling path — the type costs nothing. */
export function getConfiguredWorktreeSharedDirectories(repoPath: string): readonly string[] {
const cached = configuredSharedDirectoriesByRepoPath.get(repoPath)
const now = Date.now()
if (cached && cached.expiresAt > now) {
return cached.directories
}
const configured = loadHooks(repoPath)?.worktree?.sharedDirectories ?? []
configuredSharedDirectoriesByRepoPath.set(repoPath, {
directories: configured,
expiresAt: now + CONFIGURED_SHARED_DIRECTORIES_CACHE_TTL_MS
})
return configured
}
/** Reset the process cache between tests. */
export function clearConfiguredWorktreeSharedDirectoriesCacheForTests(): void {
configuredSharedDirectoriesByRepoPath.clear()
}
/** Every path Orca may have symlinked into a worktree: the per-user Worktree
* Shared Paths setting plus the repo's `orca.yaml` shared directories.
*
* Callers pair this with `findExistingWorktreeSymlinkPaths`, which keeps only
* the entries that really are symlinks — so a configured name that the user
* happens to own as a regular file is never treated as one of ours. */
export function getWorktreeSharedLinkPaths(repo: Pick<Repo, 'path' | 'symlinkPaths'>): string[] {
return Array.from(
new Set([...(repo.symlinkPaths ?? []), ...getConfiguredWorktreeSharedDirectories(repo.path)])
)
}
/** Resolve `worktree.sharedDirectories` from the repo-root `orca.yaml` to
* concrete repo-relative directories to symlink into a new worktree.
*
* Only directories that exist in the primary checkout **and** are gitignored are
* returned: tracked directories are already materialized by the checkout, and
* sharing an unignored path would surface the link as a spurious worktree diff.
*
* Never throws — any read/parse/git failure resolves to `[]` so worktree
* creation is never blocked by this file. */
export async function resolveWorktreeSharedDirectories(
repoPath: string,
options: GitRuntimeOptions = {}
): Promise<string[]> {
try {
const configured = loadHooks(repoPath)?.worktree?.sharedDirectories ?? []
if (configured.length === 0) {
return []
}
// Keep only entries that exist as directories; a listed but absent path
// (node_modules before install) has nothing to share.
const existing: string[] = []
for (const relativePath of configured) {
try {
if ((await stat(join(repoPath, relativePath))).isDirectory()) {
existing.push(relativePath)
} else {
console.warn(
`[worktree-shared-directories] Skipping "${relativePath}": sharedDirectories entries must be directories`
)
}
} catch {
// Absent in the primary checkout — nothing to share.
}
}
if (existing.length === 0) {
return []
}
const ignored = new Set(await checkIgnoredPaths(repoPath, existing, options))
for (const relativePath of existing) {
if (!ignored.has(relativePath)) {
console.warn(
`[worktree-shared-directories] Skipping "${relativePath}": only gitignored directories can be shared`
)
}
}
return existing.filter((relativePath) => ignored.has(relativePath)).sort()
} catch (error) {
console.warn('[worktree-shared-directories] Failed to resolve shared directories:', error)
return []
}
}