/**
* Where development state lives, and how to keep it away from the shared
* `~/.t3` that a user's installed T3 Code runs against.
*
* A linked git worktree gets its own (gitignored) `.t3`: feature work in a
* throwaway branch must not share a database with the real app, and an ambient
* `T3CODE_HOME` counts as an explicit base dir — flipping the state directory
* from `/dev` to `/userdata`, the live production database.
*/
import * as Effect from "effect/Effect";
import * as FileSystem from "effect/FileSystem";
import * as Option from "effect/Option";
import * as Path from "effect/Path";
/**
* A `.git` file points at the real git directory. A linked worktree's lives at
* `/worktrees/`; a submodule's at
* `/modules/`. Both are files, so the pointer — not the
* file-vs-directory distinction alone — is what identifies a worktree.
*
* The common dir is not necessarily named `.git`: a worktree of a bare repo
* points at `.git/worktrees/`, and `$GIT_COMMON_DIR` can be
* anything. So match on the `worktrees/` tail, which git always uses,
* rather than on the name of the directory containing it.
*/
const pointsAtLinkedWorktree = (gitFileContents: string, path: Path.Path): boolean => {
const gitdir = gitFileContents
.split(/\r?\n/)
.map((line) => line.trim())
.find((line) => line.startsWith("gitdir:"))
?.slice("gitdir:".length)
.trim();
if (gitdir === undefined || gitdir.length === 0) {
return false;
}
// Compare as path segments so a directory merely named `…worktrees…` cannot
// match as a substring. Trailing separators normalize away first.
const segments = path
.normalize(gitdir.replaceAll("\\", "/"))
.split(/[/\\]/)
.filter((segment) => segment.length > 0);
// `/worktrees/`: `worktrees` is the penultimate segment,
// and something must precede it. This excludes `/modules/`.
return segments.length >= 3 && segments.at(-2) === "worktrees";
};
/**
* The path of the linked git worktree containing `cwd`, or undefined when
* `cwd` is not inside one. Git marks a linked worktree by making `.git` a file
* whose `gitdir:` points into the repository's `.git/worktrees`.
*
* Walks up to the repository root, so running from a subdirectory resolves the
* same worktree as running from the top.
*/
export const resolveGitWorktreePath = (
cwd: string,
): Effect.Effect =>
Effect.gen(function* () {
const fileSystem = yield* FileSystem.FileSystem;
const path = yield* Path.Path;
let directory = path.resolve(cwd);
for (;;) {
const gitPath = path.join(directory, ".git");
const info = yield* fileSystem.stat(gitPath).pipe(Effect.option);
if (Option.isSome(info)) {
// A directory means the main checkout. Stop either way: nesting one
// repository inside another does not make the outer one this root.
if (info.value.type !== "File") {
return undefined;
}
// A submodule also has a `.git` file, but it is not a worktree of this
// repository and gets no worktree-local home.
const contents = yield* fileSystem
.readFileString(gitPath)
.pipe(Effect.orElseSucceed(() => ""));
return pointsAtLinkedWorktree(contents, path) ? directory : undefined;
}
const parent = path.dirname(directory);
if (parent === directory) {
return undefined;
}
directory = parent;
}
});
/**
* The worktree-local data directory for `cwd`, or undefined outside a linked
* worktree. Deliberately does not require the directory to exist yet: falling
* back because it is missing would send callers at the shared home.
*/
export const resolveWorktreeT3Home = (
cwd: string,
): Effect.Effect =>
Effect.gen(function* () {
const worktreePath = yield* resolveGitWorktreePath(cwd);
if (worktreePath === undefined) {
return undefined;
}
const path = yield* Path.Path;
return path.join(worktreePath, ".t3");
});