| 1 | /** |
| 2 | * Where development state lives, and how to keep it away from the shared |
| 3 | * `~/.t3` that a user's installed T3 Code runs against. |
| 4 | * |
| 5 | * A linked git worktree gets its own (gitignored) `.t3`: feature work in a |
| 6 | * throwaway branch must not share a database with the real app, and an ambient |
| 7 | * `T3CODE_HOME` counts as an explicit base dir — flipping the state directory |
| 8 | * from `<base>/dev` to `<base>/userdata`, the live production database. |
| 9 | */ |
| 10 | |
| 11 | import * as Effect from "effect/Effect"; |
| 12 | import * as FileSystem from "effect/FileSystem"; |
| 13 | import * as Option from "effect/Option"; |
| 14 | import * as Path from "effect/Path"; |
| 15 | |
| 16 | /** |
| 17 | * A `.git` file points at the real git directory. A linked worktree's lives at |
| 18 | * `<common-dir>/worktrees/<name>`; a submodule's at |
| 19 | * `<super-git-dir>/modules/<name>`. Both are files, so the pointer — not the |
| 20 | * file-vs-directory distinction alone — is what identifies a worktree. |
| 21 | * |
| 22 | * The common dir is not necessarily named `.git`: a worktree of a bare repo |
| 23 | * points at `<repo>.git/worktrees/<name>`, and `$GIT_COMMON_DIR` can be |
| 24 | * anything. So match on the `worktrees/<name>` tail, which git always uses, |
| 25 | * rather than on the name of the directory containing it. |
| 26 | */ |
| 27 | const pointsAtLinkedWorktree = (gitFileContents: string, path: Path.Path): boolean => { |
| 28 | const gitdir = gitFileContents |
| 29 | .split(/\r?\n/) |
| 30 | .map((line) => line.trim()) |
| 31 | .find((line) => line.startsWith("gitdir:")) |
| 32 | ?.slice("gitdir:".length) |
| 33 | .trim(); |
| 34 | if (gitdir === undefined || gitdir.length === 0) { |
| 35 | return false; |
| 36 | } |
| 37 | // Compare as path segments so a directory merely named `…worktrees…` cannot |
| 38 | // match as a substring. Trailing separators normalize away first. |
| 39 | const segments = path |
| 40 | .normalize(gitdir.replaceAll("\\", "/")) |
| 41 | .split(/[/\\]/) |
| 42 | .filter((segment) => segment.length > 0); |
| 43 | // `<common-dir>/worktrees/<name>`: `worktrees` is the penultimate segment, |
| 44 | // and something must precede it. This excludes `<git-dir>/modules/<name>`. |
| 45 | return segments.length >= 3 && segments.at(-2) === "worktrees"; |
| 46 | }; |
| 47 | |
| 48 | /** |
| 49 | * The path of the linked git worktree containing `cwd`, or undefined when |
| 50 | * `cwd` is not inside one. Git marks a linked worktree by making `.git` a file |
| 51 | * whose `gitdir:` points into the repository's `.git/worktrees`. |
| 52 | * |
| 53 | * Walks up to the repository root, so running from a subdirectory resolves the |
| 54 | * same worktree as running from the top. |
| 55 | */ |
| 56 | export const resolveGitWorktreePath = ( |
| 57 | cwd: string, |
| 58 | ): Effect.Effect<string | undefined, never, FileSystem.FileSystem | Path.Path> => |
| 59 | Effect.gen(function* () { |
| 60 | const fileSystem = yield* FileSystem.FileSystem; |
| 61 | const path = yield* Path.Path; |
| 62 | |
| 63 | let directory = path.resolve(cwd); |
| 64 | for (;;) { |
| 65 | const gitPath = path.join(directory, ".git"); |
| 66 | const info = yield* fileSystem.stat(gitPath).pipe(Effect.option); |
| 67 | if (Option.isSome(info)) { |
| 68 | // A directory means the main checkout. Stop either way: nesting one |
| 69 | // repository inside another does not make the outer one this root. |
| 70 | if (info.value.type !== "File") { |
| 71 | return undefined; |
| 72 | } |
| 73 | // A submodule also has a `.git` file, but it is not a worktree of this |
| 74 | // repository and gets no worktree-local home. |
| 75 | const contents = yield* fileSystem |
| 76 | .readFileString(gitPath) |
| 77 | .pipe(Effect.orElseSucceed(() => "")); |
| 78 | return pointsAtLinkedWorktree(contents, path) ? directory : undefined; |
| 79 | } |
| 80 | const parent = path.dirname(directory); |
| 81 | if (parent === directory) { |
| 82 | return undefined; |
| 83 | } |
| 84 | directory = parent; |
| 85 | } |
| 86 | }); |
| 87 | |
| 88 | /** |
| 89 | * The worktree-local data directory for `cwd`, or undefined outside a linked |
| 90 | * worktree. Deliberately does not require the directory to exist yet: falling |
| 91 | * back because it is missing would send callers at the shared home. |
| 92 | */ |
| 93 | export const resolveWorktreeT3Home = ( |
| 94 | cwd: string, |
| 95 | ): Effect.Effect<string | undefined, never, FileSystem.FileSystem | Path.Path> => |
| 96 | Effect.gen(function* () { |
| 97 | const worktreePath = yield* resolveGitWorktreePath(cwd); |
| 98 | if (worktreePath === undefined) { |
| 99 | return undefined; |
| 100 | } |
| 101 | const path = yield* Path.Path; |
| 102 | return path.join(worktreePath, ".t3"); |
| 103 | }); |