mirror of
https://github.com/garrytan/gstack.git
synced 2026-09-21 12:20:48 +02:00
implement six-skill gstack 2 runtime
This commit is contained in:
@@ -0,0 +1,108 @@
|
||||
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;
|
||||
}
|
||||
Reference in New Issue
Block a user