Z ZPI 文档 v0.3.8
介绍页

源码

packages/desktop/src/main/worktrees.ts

typescript91 行查看原文
import { execFile } from "node:child_process";
import { randomBytes } from "node:crypto";
import { mkdir, rmdir } from "node:fs/promises";
import { homedir } from "node:os";
import { basename, join } from "node:path";
import { promisify } from "node:util";
import type { GitEnvironment } from "../shared/bridge.ts";

const exec = promisify(execFile);
async function git(cwd: string, args: string[]) {
  return (await exec("git", ["-C", cwd, ...args], { timeout: 30_000, maxBuffer: 1024 * 1024 })).stdout.trim();
}
export async function gitEnvironment(cwd: string): Promise<GitEnvironment | null> {
  try {
    if ((await git(cwd, ["rev-parse", "--is-inside-work-tree"])) !== "true") return null;
    const [head, branch, refs] = await Promise.all([
      git(cwd, ["rev-parse", "--verify", "HEAD"]),
      git(cwd, ["branch", "--show-current"]),
      git(cwd, ["for-each-ref", "--format=%(refname)%09%(symref)", "refs/heads/", "refs/remotes/"]),
    ]);
    const branches = refs
      .split("\n")
      .filter(Boolean)
      .flatMap((line) => {
        const [ref, symbolic] = line.split("\t");
        if (symbolic) return [];
        const remote = ref.startsWith("refs/remotes/");
        return [{ ref, name: ref.slice(remote ? "refs/remotes/".length : "refs/heads/".length), remote }];
      });
    return {
      branch: branch || head.slice(0, 7),
      head,
      currentRef: branch ? `refs/heads/${branch}` : null,
      branches,
    };
  } catch {
    return null;
  }
}
export async function resolveWorktreeBase(cwd: string, ref: string): Promise<string> {
  if (typeof ref !== "string" || (ref !== "HEAD" && !/^refs\/(heads|remotes)\//.test(ref)))
    throw new Error("invalid_input: 无效起始分支");
  if (ref !== "HEAD") {
    try {
      await git(cwd, ["check-ref-format", ref]);
    } catch {
      throw new Error("invalid_input: 无效起始分支");
    }
  }
  try {
    return await git(cwd, ["rev-parse", "--verify", "--end-of-options", `${ref}^{commit}`]);
  } catch {
    throw new Error("configuration: 起始分支不存在或没有可用提交,请重新选择");
  }
}
export async function createWorktree(
  sourceCwd: string,
  root = join(homedir(), ".zpi", "worktree"),
  onProgress?: (message: string) => void,
  baseRef?: string,
) {
  onProgress?.("正在检查 Git 项目…");
  const environment = await gitEnvironment(sourceCwd);
  if (!environment) throw new Error("configuration: 新工作树需要已有提交的 Git 项目");
  const head = baseRef === undefined ? environment.head : await resolveWorktreeBase(sourceCwd, baseRef);
  const repository = await git(sourceCwd, ["rev-parse", "--show-toplevel"]);
  await mkdir(root, { recursive: true });
  for (let attempt = 0; attempt < 32; attempt++) {
    const id = randomBytes(2).toString("hex");
    const directory = join(root, id);
    const path = join(directory, basename(repository));
    try {
      await mkdir(directory);
    } catch (error) {
      if ((error as NodeJS.ErrnoException).code === "EEXIST") continue;
      throw error;
    }
    // Resolve once so changes to the source branch during checkout cannot change the base.
    onProgress?.(`正在创建工作树 · ${id}…`);
    try {
      await git(repository, ["worktree", "add", "--detach", path, head]);
    } catch (error) {
      // Only remove our empty reservation; Git may have left a partial checkout.
      await rmdir(directory).catch(() => {});
      throw error;
    }
    return { id, path, sourceCwd };
  }
  throw new Error("storage: 无法分配工作树名称,请重试");
}