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);
}