mirror of
https://github.com/garrytan/gstack.git
synced 2026-09-21 12:20:48 +02:00
109 lines
3.7 KiB
JavaScript
109 lines
3.7 KiB
JavaScript
import fs from "node:fs/promises";
|
|
import path from "node:path";
|
|
import { createHash } from "node:crypto";
|
|
import { execFile as execFileCallback } from "node:child_process";
|
|
import { promisify } from "node:util";
|
|
|
|
const execFile = promisify(execFileCallback);
|
|
|
|
export function stableId(namespace, value, length = 20) {
|
|
const digest = createHash("sha256")
|
|
.update(`gstack:${namespace}:v2\0`, "utf8")
|
|
.update(String(value), "utf8")
|
|
.digest("hex")
|
|
.slice(0, length);
|
|
return `${namespace}_${digest}`;
|
|
}
|
|
|
|
export async function discoverProjectIdentity(cwd = process.cwd(), options = {}) {
|
|
const absoluteCwd = await canonicalPath(cwd);
|
|
const git = options.git ?? runGit;
|
|
try {
|
|
const [worktreeRootRaw, commonDirRaw, gitDirRaw] = await Promise.all([
|
|
git(["rev-parse", "--show-toplevel"], absoluteCwd),
|
|
git(["rev-parse", "--git-common-dir"], absoluteCwd),
|
|
git(["rev-parse", "--git-dir"], absoluteCwd),
|
|
]);
|
|
const worktreeRoot = await canonicalPath(resolveGitPath(worktreeRootRaw, absoluteCwd));
|
|
const commonDir = await canonicalPath(resolveGitPath(commonDirRaw, worktreeRoot));
|
|
const gitDir = await canonicalPath(resolveGitPath(gitDirRaw, worktreeRoot));
|
|
return identityFromPaths({ worktreeRoot, commonDir, gitDir, isGit: true });
|
|
} catch (error) {
|
|
if (options.requireGit) throw error;
|
|
if (!isNotGitRepository(error)) throw error;
|
|
return identityFromPaths({
|
|
worktreeRoot: absoluteCwd,
|
|
commonDir: absoluteCwd,
|
|
gitDir: absoluteCwd,
|
|
isGit: false,
|
|
});
|
|
}
|
|
}
|
|
|
|
export function identityFromPaths({ worktreeRoot, commonDir, gitDir, isGit = true }) {
|
|
const resolvedRoot = path.resolve(worktreeRoot);
|
|
const resolvedCommon = path.resolve(commonDir);
|
|
const resolvedGitDir = path.resolve(gitDir);
|
|
const normalizedRoot = normalizeIdentityPath(resolvedRoot);
|
|
const normalizedCommon = normalizeIdentityPath(resolvedCommon);
|
|
const repoId = stableId("repo", normalizedCommon);
|
|
|
|
// Linked worktrees have a durable git-dir slot under the common repository.
|
|
// The Git slot is stable when a linked checkout moves and unique within the
|
|
// common repository. Non-Git folders have no slot, so their canonical path
|
|
// remains the identity boundary.
|
|
const gitSlot = path.relative(resolvedCommon, resolvedGitDir) || ".";
|
|
const worktreeId = stableId(
|
|
"worktree",
|
|
isGit ? `${repoId}\0${normalizeIdentityPath(gitSlot)}` : `${repoId}\0${normalizedRoot}`,
|
|
);
|
|
const projectId = stableId("project", `${repoId}\0${worktreeId}`, 24);
|
|
return Object.freeze({
|
|
projectId,
|
|
repoId,
|
|
worktreeId,
|
|
worktreeRoot: resolvedRoot,
|
|
repoCommonDir: resolvedCommon,
|
|
gitDir: resolvedGitDir,
|
|
isGit,
|
|
});
|
|
}
|
|
|
|
function isNotGitRepository(error) {
|
|
if (error?.code === "NOT_GIT") return true;
|
|
const detail = `${error?.stderr ?? ""}\n${error?.message ?? ""}`;
|
|
return (error?.code === 128 || error?.exitCode === 128) && /not a git repository/i.test(detail);
|
|
}
|
|
|
|
async function runGit(args, cwd) {
|
|
const { stdout } = await execFile("git", args, {
|
|
cwd,
|
|
encoding: "utf8",
|
|
timeout: 5_000,
|
|
maxBuffer: 1024 * 1024,
|
|
windowsHide: true,
|
|
});
|
|
return stdout.replace(/[\r\n]+$/, "");
|
|
}
|
|
|
|
function resolveGitPath(value, cwd) {
|
|
if (!value) throw new Error("git returned an empty path");
|
|
return path.isAbsolute(value) ? value : path.resolve(cwd, value);
|
|
}
|
|
|
|
async function canonicalPath(value) {
|
|
const absolute = path.resolve(value);
|
|
try {
|
|
return await fs.realpath(absolute);
|
|
} catch (error) {
|
|
if (error?.code === "ENOENT") return absolute;
|
|
throw error;
|
|
}
|
|
}
|
|
|
|
function normalizeIdentityPath(value) {
|
|
let normalized = path.normalize(String(value)).replaceAll(path.sep, "/");
|
|
if (process.platform === "win32") normalized = normalized.toLowerCase();
|
|
return normalized;
|
|
}
|