| 1 | import { execFileSync, spawnSync } from "node:child_process"; |
| 2 | import { existsSync, readFileSync, readdirSync, realpathSync } from "node:fs"; |
| 3 | import { 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. |
| 7 | const env = { ...process.env }; |
| 8 | for (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 | |
| 16 | function git(args, cwd) { |
| 17 | return execFileSync("git", args, { |
| 18 | cwd, |
| 19 | env, |
| 20 | encoding: "utf8", |
| 21 | stdio: ["ignore", "pipe", "inherit"], |
| 22 | }).trim(); |
| 23 | } |
| 24 | |
| 25 | function tryGit(args, cwd) { |
| 26 | const result = spawnSync("git", args, { cwd, env, encoding: "utf8" }); |
| 27 | return result.status === 0 ? result.stdout.trim() : undefined; |
| 28 | } |
| 29 | |
| 30 | export 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 | |
| 115 | function 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 | |
| 168 | function 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. |
| 178 | if (import.meta.main && process.argv[4] === "1") { |
| 179 | bootstrap( |
| 180 | resolve(import.meta.dirname, ".."), |
| 181 | process.argv[2], |
| 182 | ); |
| 183 | } |