File size: 2,861 Bytes
e249c6d | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 | import { requireGitCommandOutput } from "../../infra/git-exec.js";
import { commandError, runGit } from "./git.js";
type ResolvedWorktreeBase = {
commit: string;
gitOperand: string;
recordRef: string;
remote: boolean;
};
export class InvalidWorktreeBaseRefError extends Error {
constructor(options?: ErrorOptions) {
super(
"Worktree base ref does not resolve to a commit. Choose a local or remote branch and retry.",
options,
);
this.name = "InvalidWorktreeBaseRefError";
}
}
export async function resolveWorktreeBase(
repoRoot: string,
baseRef?: string,
signal?: AbortSignal,
assertCurrent?: () => void,
): Promise<ResolvedWorktreeBase> {
if (baseRef) {
const verified = await runGit(
repoRoot,
[
"-c",
"core.warnAmbiguousRefs=true",
"rev-parse",
"--verify",
"--end-of-options",
`${baseRef === "-" ? "@{-1}" : baseRef}^{commit}`,
],
{ signal, beforeRun: assertCurrent },
);
signal?.throwIfAborted();
if (
verified.termination === "exit" &&
typeof verified.code === "number" &&
verified.code !== 0
) {
throw new InvalidWorktreeBaseRefError({
cause: commandError("git rev-parse --verify", verified),
});
}
const commit = requireGitCommandOutput("git rev-parse --verify", verified).trim();
if (!commit || commit.includes("\n") || verified.stderr.trim()) {
throw new InvalidWorktreeBaseRefError({
cause: commandError("git rev-parse --verify", verified),
});
}
// `worktree add -b` forwards its start point to `git branch`, which parses
// options again without another `--`; pass the verified commit for dashed refs.
const gitOperand = baseRef !== "-" && baseRef.startsWith("-") ? commit : baseRef;
return { commit, gitOperand, recordRef: baseRef, remote: false };
}
const fetched = await runGit(repoRoot, ["fetch", "origin"], { signal, beforeRun: assertCurrent });
signal?.throwIfAborted();
if (fetched.termination === "exit" && fetched.code === 0) {
const remoteHead = await runGit(
repoRoot,
["symbolic-ref", "--quiet", "--short", "refs/remotes/origin/HEAD"],
{ signal, beforeRun: assertCurrent },
);
if (remoteHead.termination === "exit" && remoteHead.code === 0 && remoteHead.stdout.trim()) {
const remoteRef = remoteHead.stdout.trim();
try {
const resolved = await resolveWorktreeBase(repoRoot, remoteRef, signal, assertCurrent);
return { ...resolved, remote: true };
} catch (error) {
// Pruning a retired default branch can leave origin/HEAD dangling.
if (!(error instanceof InvalidWorktreeBaseRefError)) {
throw error;
}
}
}
}
return await resolveWorktreeBase(repoRoot, "HEAD", signal, assertCurrent);
}
|