1import { execFileSync, spawnSync } from "node:child_process";
2import { existsSync, readFileSync, readdirSync, realpathSync } from "node:fs";
3import { dirname, resolve } from "node:path";
4
5// Git hooks export repository-specific variables. Do not pass those through to
6// commands operating on distilled or a different Alchemy worktree.
7const env = { ...process.env };
8for (const name of execFileSync("git", ["rev-parse", "--local-env-vars"], {
9 encoding: "utf8",
10})
11 .trim()
12 .split("\n")) {
13 delete env[name];
14}
15
16function git(args, cwd) {
17 return execFileSync("git", args, {
18 cwd,
19 env,
20 encoding: "utf8",
21 stdio: ["ignore", "pipe", "inherit"],
22 }).trim();
23}
24
25function tryGit(args, cwd) {
26 const result = spawnSync("git", args, { cwd, env, encoding: "utf8" });
27 return result.status === 0 ? result.stdout.trim() : undefined;
28}
29
30export function bootstrap(root, previousHead) {
31 const checkout = resolve(root, "submodules/distilled");
32 // Use the index, just like `git submodule update`, including a staged pin.
33 const pin = git(["rev-parse", ":submodules/distilled"], root);
34 const commonDir = git(
35 ["rev-parse", "--path-format=absolute", "--git-common-dir"],
36 root,
37 );
38 const gitDir = git(
39 ["rev-parse", "--path-format=absolute", "--git-dir"],
40 root,
41 );
42 const mainRoot = git(["worktree", "list", "--porcelain", "-z"], root)
43 .split("\0")[0]
44 .slice("worktree ".length);
45 if (previousHead !== undefined) isolateWorktreeConfig(mainRoot);
46
47 if (existsSync(resolve(checkout, ".git"))) {
48 if (
49 realpathSync(git(["rev-parse", "--show-toplevel"], checkout)) !==
50 realpathSync(checkout)
51 ) {
52 throw new Error(
53 `Cannot bootstrap distilled: invalid checkout at ${checkout}`,
54 );
55 }
56 const current = git(["rev-parse", "HEAD"], checkout);
57 if (current === pin) return;
58
59 const previousPin = previousHead
60 ? tryGit(["rev-parse", `${previousHead}:submodules/distilled`], root)
61 : undefined;
62 // Only follow an Alchemy checkout when distilled is still at its old pin.
63 // Intentional distilled development never moves an existing HEAD.
64 if (
65 current !== previousPin ||
66 tryGit(["symbolic-ref", "--quiet", "HEAD"], checkout) !== undefined ||
67 git(["status", "--porcelain"], checkout) !== ""
68 ) {
69 console.warn(
70 `Preserving distilled at ${current}; Alchemy pins ${pin}. To sync explicitly, run: git -C "${root}" submodule update --init --checkout -- submodules/distilled`,
71 );
72 return;
73 }
74 ensureCommit(checkout, pin);
75 git(["checkout", "--detach", pin], checkout);
76 return;
77 }
78
79 if (existsSync(checkout) && readdirSync(checkout).length !== 0) {
80 throw new Error(
81 `Cannot bootstrap distilled: ${checkout} is nonempty but has no .git entry`,
82 );
83 }
84
85 if (gitDir === commonDir) {
86 git(
87 [
88 "submodule",
89 "update",
90 "--init",
91 "--checkout",
92 "--",
93 "submodules/distilled",
94 ],
95 root,
96 );
97 if (previousHead !== undefined) isolateWorktreeConfig(mainRoot);
98 return;
99 }
100
101 // Initialize the primary checkout at its own pin first. Every linked checkout
102 // then gets an independent detached HEAD backed by that same object database.
103 bootstrap(mainRoot);
104 isolateWorktreeConfig(mainRoot);
105 const mainCheckout = resolve(mainRoot, "submodules/distilled");
106 ensureCommit(mainCheckout, pin);
107 // The destination was checked above: it is missing or empty. One --force
108 // replaces only its stale registration; Git still refuses locked worktrees
109 // (overriding a lock would require --force twice). Do not prune other entries.
110 git(["worktree", "add", "--force", "--detach", checkout, pin], mainCheckout);
111 isolateWorktreeConfig(mainRoot);
112 git(["submodule", "init", "--", "submodules/distilled"], root);
113}
114
115function isolateWorktreeConfig(mainRoot) {
116 const checkout = resolve(mainRoot, "submodules/distilled");
117 if (!existsSync(resolve(checkout, ".git"))) return;
118
119 // Resolve the gitfile without opening the repository: its core.worktree may
120 // already point at a removed Alchemy worktree, preventing normal Git commands.
121 const commonDir = resolve(
122 checkout,
123 git(["rev-parse", "--resolve-git-dir", ".git"], checkout),
124 );
125 if (existsSync(resolve(commonDir, "commondir"))) {
126 throw new Error(
127 "The primary distilled checkout must not be a linked worktree",
128 );
129 }
130 const configs = [[commonDir, checkout]];
131 const worktrees = resolve(commonDir, "worktrees");
132 if (existsSync(worktrees)) {
133 for (const entry of readdirSync(worktrees)) {
134 const admin = resolve(worktrees, entry);
135 const gitfile = readFileSync(resolve(admin, "gitdir"), "utf8").trim();
136 configs.push([admin, dirname(resolve(admin, gitfile))]);
137 }
138 }
139 // Protect every registered worktree, including temporarily missing/locked
140 // ones. Submodule sync/update can write core.worktree to the shared config;
141 // explicit per-worktree overrides prevent that from redirecting any checkout.
142 for (const [admin, path] of configs) {
143 git(
144 [
145 "config",
146 "--file",
147 resolve(admin, "config.worktree"),
148 "core.worktree",
149 path,
150 ],
151 mainRoot,
152 );
153 }
154 const config = resolve(commonDir, "config");
155 git(
156 ["config", "--file", config, "extensions.worktreeConfig", "true"],
157 mainRoot,
158 );
159 // New worktrees must not inherit a path before their override is installed.
160 if (
161 tryGit(["config", "--file", config, "--get", "core.worktree"], mainRoot) !==
162 undefined
163 ) {
164 git(["config", "--file", config, "--unset-all", "core.worktree"], mainRoot);
165 }
166}
167
168function ensureCommit(checkout, pin) {
169 if (tryGit(["cat-file", "-e", `${pin}^{commit}`], checkout) === undefined) {
170 // Never replace an unavailable pin with a branch tip.
171 git(["fetch", "origin", pin], checkout);
172 git(["cat-file", "-e", `${pin}^{commit}`], checkout);
173 }
174}
175
176// Run only for post-checkout branch/worktree checkouts, not file checkouts.
177// Hook arguments are old HEAD, new HEAD, and the branch-checkout flag.
178if (import.meta.main && process.argv[4] === "1") {
179 bootstrap(
180 resolve(import.meta.dirname, ".."),
181 process.argv[2],
182 );
183}