+231
@@ -0,0 +1,231 @@
|
||||
import { execFile } from "node:child_process";
|
||||
import { realpath } from "node:fs/promises";
|
||||
import { z } from "zod";
|
||||
|
||||
export async function command(args: string[], cwd: string, signal: AbortSignal) {
|
||||
return new Promise<string>((resolve, reject) => {
|
||||
execFile(
|
||||
args[0]!,
|
||||
args.slice(1),
|
||||
{ cwd, signal, timeout: 30_000, maxBuffer: 2_000_000 },
|
||||
(error, stdout, stderr) => {
|
||||
if (error) reject(new Error(`${args[0]} failed: ${stderr || error.message}`));
|
||||
else resolve(stdout.trim());
|
||||
},
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
const inventory = z.object({
|
||||
result: z.object({
|
||||
source: z.object({
|
||||
repo_key: z.string(),
|
||||
repo_root: z.string(),
|
||||
source_checkout_path: z.string(),
|
||||
source_workspace_id: z.string(),
|
||||
}),
|
||||
worktrees: z.array(
|
||||
z.object({
|
||||
path: z.string(),
|
||||
is_linked_worktree: z.boolean(),
|
||||
is_bare: z.boolean(),
|
||||
open_workspace_id: z.string().nullish(),
|
||||
}),
|
||||
),
|
||||
}),
|
||||
});
|
||||
const workspaces = z.object({
|
||||
result: z.object({ workspaces: z.array(z.object({ workspace_id: z.string() })) }),
|
||||
});
|
||||
const created = z.object({
|
||||
result: z.object({
|
||||
workspace: z.object({ workspace_id: z.string() }),
|
||||
worktree: z.object({
|
||||
path: z.string(),
|
||||
branch: z.string(),
|
||||
is_linked_worktree: z.literal(true),
|
||||
}),
|
||||
}),
|
||||
});
|
||||
export const rootSchema = z.object({
|
||||
directory: z.string(),
|
||||
workspaceID: z.string(),
|
||||
repo: z.string(),
|
||||
});
|
||||
export type HerdrRoot = z.infer<typeof rootSchema>;
|
||||
|
||||
// The command seam also lets tests use an isolated CLI without touching a live server.
|
||||
export class Herdr {
|
||||
constructor(private readonly run = command) {}
|
||||
|
||||
async root(directory: string, signal: AbortSignal): Promise<HerdrRoot | undefined> {
|
||||
signal = AbortSignal.any([signal, AbortSignal.timeout(5_000)]);
|
||||
const canonical = await realpath(directory);
|
||||
const list = workspaces.parse(
|
||||
JSON.parse(await this.run(["herdr", "workspace", "list"], canonical, signal)),
|
||||
);
|
||||
const matches: HerdrRoot[] = [];
|
||||
for (const workspace of list.result.workspaces) {
|
||||
let value: z.infer<typeof inventory>;
|
||||
try {
|
||||
value = inventory.parse(
|
||||
JSON.parse(
|
||||
await this.run(
|
||||
["herdr", "worktree", "list", "--workspace", workspace.workspace_id],
|
||||
canonical,
|
||||
signal,
|
||||
),
|
||||
),
|
||||
);
|
||||
} catch (error) {
|
||||
if (signal.aborted) throw error;
|
||||
// Non-repository workspaces cannot provide a worktree inventory.
|
||||
continue;
|
||||
}
|
||||
const source = value.result.source;
|
||||
if (source.source_workspace_id !== workspace.workspace_id) continue;
|
||||
if (
|
||||
(await realpath(source.source_checkout_path)) !== canonical ||
|
||||
(await realpath(source.repo_root)) !== canonical
|
||||
)
|
||||
continue;
|
||||
const checkout = value.result.worktrees.find(
|
||||
(item) =>
|
||||
item.open_workspace_id === workspace.workspace_id &&
|
||||
!item.is_linked_worktree &&
|
||||
!item.is_bare,
|
||||
);
|
||||
if (!checkout || (await realpath(checkout.path)) !== canonical) continue;
|
||||
const repo = await realpath(source.repo_key);
|
||||
const gitRepo = await this.run(
|
||||
["git", "rev-parse", "--path-format=absolute", "--git-common-dir"],
|
||||
canonical,
|
||||
signal,
|
||||
);
|
||||
if ((await realpath(gitRepo)) !== repo) continue;
|
||||
matches.push({ directory: canonical, workspaceID: workspace.workspace_id, repo });
|
||||
}
|
||||
// Never guess which Herdr workspace owns a duplicated checkout.
|
||||
return matches.length === 1 ? matches[0] : undefined;
|
||||
}
|
||||
|
||||
async create(root: HerdrRoot, base: string, branch: string, signal: AbortSignal) {
|
||||
const current = await this.root(root.directory, signal);
|
||||
if (!current || current.workspaceID !== root.workspaceID || current.repo !== root.repo)
|
||||
throw new Error("Herdr root workspace is no longer eligible");
|
||||
const ref = await this.run(
|
||||
["git", "rev-parse", "--symbolic-full-name", "--verify", "--end-of-options", base],
|
||||
root.directory,
|
||||
signal,
|
||||
);
|
||||
if (!ref.startsWith("refs/heads/") && !ref.startsWith("refs/remotes/"))
|
||||
throw new Error(
|
||||
"base must identify a locally available branch or remote-tracking branch",
|
||||
);
|
||||
const commit = await this.run(
|
||||
["git", "rev-parse", "--verify", "--end-of-options", `${ref}^{commit}`],
|
||||
root.directory,
|
||||
signal,
|
||||
);
|
||||
await this.run(["git", "check-ref-format", "--branch", branch], root.directory, signal);
|
||||
if (
|
||||
await this.run(
|
||||
["git", "branch", "--list", "--format=%(refname)", branch],
|
||||
root.directory,
|
||||
signal,
|
||||
)
|
||||
)
|
||||
throw new Error(`Destination branch already exists: ${branch}`);
|
||||
const response = created.parse(
|
||||
JSON.parse(
|
||||
await this.run(
|
||||
[
|
||||
"herdr",
|
||||
"worktree",
|
||||
"create",
|
||||
// Pin the repository even if a user repoints the workspace after discovery.
|
||||
"--cwd",
|
||||
root.directory,
|
||||
"--branch",
|
||||
branch,
|
||||
"--base",
|
||||
commit,
|
||||
"--no-focus",
|
||||
],
|
||||
root.directory,
|
||||
signal,
|
||||
),
|
||||
),
|
||||
);
|
||||
const directory = await realpath(response.result.worktree.path);
|
||||
const workspaceID = response.result.workspace.workspace_id;
|
||||
try {
|
||||
if (
|
||||
response.result.worktree.branch !== branch ||
|
||||
directory === root.directory ||
|
||||
workspaceID === root.workspaceID
|
||||
)
|
||||
throw new Error("Herdr returned an unexpected checkout");
|
||||
const actual = await this.run(["git", "rev-parse", "HEAD"], directory, signal);
|
||||
const actualBranch = await this.run(
|
||||
["git", "symbolic-ref", "--short", "HEAD"],
|
||||
directory,
|
||||
signal,
|
||||
);
|
||||
const repo = await this.run(
|
||||
["git", "rev-parse", "--path-format=absolute", "--git-common-dir"],
|
||||
directory,
|
||||
signal,
|
||||
);
|
||||
if (
|
||||
actual !== commit ||
|
||||
actualBranch !== branch ||
|
||||
(await realpath(repo)) !== root.repo
|
||||
)
|
||||
throw new Error(
|
||||
"Created checkout does not match the requested repository and base",
|
||||
);
|
||||
await this.verify(directory, workspaceID, root.repo, branch, signal);
|
||||
} catch (error) {
|
||||
throw new Error(
|
||||
`Retained Herdr workspace ${workspaceID}, checkout ${directory}, branch ${branch}`,
|
||||
{ cause: error },
|
||||
);
|
||||
}
|
||||
return { directory, workspaceID, branch };
|
||||
}
|
||||
|
||||
async verify(
|
||||
directory: string,
|
||||
workspaceID: string,
|
||||
repo: string,
|
||||
branch: string,
|
||||
signal: AbortSignal,
|
||||
) {
|
||||
const value = inventory.parse(
|
||||
JSON.parse(
|
||||
await this.run(
|
||||
["herdr", "worktree", "list", "--workspace", workspaceID],
|
||||
directory,
|
||||
signal,
|
||||
),
|
||||
),
|
||||
);
|
||||
const checkout = value.result.worktrees.find(
|
||||
(item) => item.open_workspace_id === workspaceID && item.is_linked_worktree,
|
||||
);
|
||||
if (
|
||||
!checkout ||
|
||||
value.result.source.source_workspace_id !== workspaceID ||
|
||||
(await realpath(value.result.source.source_checkout_path)) !== directory ||
|
||||
(await realpath(checkout.path)) !== directory ||
|
||||
(await realpath(value.result.source.repo_key)) !== repo
|
||||
)
|
||||
throw new Error("Bound Herdr checkout is no longer available");
|
||||
if (
|
||||
(await this.run(["git", "symbolic-ref", "--short", "HEAD"], directory, signal)) !==
|
||||
branch
|
||||
)
|
||||
throw new Error("Bound Herdr checkout changed branches");
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user