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
11import * as Effect from "effect/Effect";
12import * as FileSystem from "effect/FileSystem";
13import * as Option from "effect/Option";
14import * 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 */
27const 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 */
56export 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 */
93export 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 });