#!/usr/bin/env node /** * 开发分支工作区初始化脚本 * * 用于创建基于远端分支或新建的开发分支工作区。 */ import { execFileSync } from "node:child_process"; import { existsSync, mkdirSync } from "node:fs"; import { resolve, relative } from "node:path"; import { createInterface } from "node:readline"; function git(args, opts) { return execFileSync("git", args, { encoding: "utf-8", stdio: "pipe", ...opts }); } function getRootDir() { try { return resolve(git(["rev-parse", "--show-toplevel"]).trim()); } catch { console.error("错误: 不在 git 仓库中"); process.exit(1); } } function fetchRemote() { try { git(["fetch", "--quiet"]); } catch { console.warn("警告: 无法获取远端信息,继续使用本地数据"); } } function listRemoteBranches() { try { return git(["branch", "-r"]) .trim() .split(/\r?\n/) .map((l) => l.trim()) .filter((l) => l && !l.includes(" -> ")); } catch { return []; } } function matchingRemoteBranches(name) { return listRemoteBranches().filter((l) => l.endsWith(`/${name}`)); } function localBranchExists(name) { try { git(["show-ref", "--verify", "--quiet", `refs/heads/${name}`]); return true; } catch { return false; } } function worktreeExists(worktreeDir) { try { const out = git(["worktree", "list"]); const target = resolve(worktreeDir); return out .split(/\r?\n/) .some((line) => { const fields = line.trim().split(/\s+/); return fields.length > 0 && resolve(fields[0]) === target; }); } catch { return false; } } function shortBranchName(remoteRef) { const idx = remoteRef.indexOf("/"); return idx === -1 ? remoteRef : remoteRef.slice(idx + 1); } function assertCanCreate(name, dir) { if (existsSync(dir)) { throw new Error(`工作区已存在于 ${dir}`); } if (worktreeExists(dir)) { throw new Error(`工作区 '${name}' 已存在`); } if (localBranchExists(name)) { throw new Error(`本地分支 '${name}' 已存在`); } } function addWorktree(name, dir, base) { const args = ["worktree", "add", "-b", name, dir]; if (base) args.push(base); git(args); } function addWorktreeSafe(name, dir, base) { assertCanCreate(name, dir); addWorktree(name, dir, base); console.log(`工作区已创建于 ${dir}`); } function ask(rl, prompt) { return new Promise((resolve) => rl.question(prompt, resolve)); } async function selectFromList(items, prompt, allowCreate) { if (items.length === 0) return null; console.log(prompt); items.forEach((item, i) => console.log(` ${i + 1}\t${item}`)); if (allowCreate) console.log(` ${items.length + 1}\t创建新分支`); console.log(); const max = allowCreate ? items.length + 1 : items.length; const rl = createInterface({ input: process.stdin, output: process.stdout }); let cancelled = false; rl.on("close", () => { cancelled = true; }); while (true) { const raw = await ask(rl, `请选择 (1-${max}): `); if (cancelled) { rl.close(); process.exit(1); } const n = Number.parseInt(raw, 10); if (Number.isNaN(n) || n < 1 || n > max) { console.log(`错误: 请输入 1-${max} 之间的数字`); continue; } if (n <= items.length) { const sel = items[n - 1]; console.log(`已选择: ${sel}`); rl.close(); return sel; } rl.close(); return null; } } async function inputBranchName() { const rl = createInterface({ input: process.stdin, output: process.stdout }); let cancelled = false; rl.on("close", () => { cancelled = true; }); while (true) { const name = (await ask(rl, "请输入新分支名称: ")).trim(); if (cancelled) { rl.close(); process.exit(1); } if (name) { rl.close(); return name; } console.log("错误: 分支名称不能为空"); } } async function handleWithName(name, worktreesDir) { const dir = resolve(worktreesDir, name); try { assertCanCreate(name, dir); } catch (e) { console.error(`错误: ${e.message}`); process.exit(1); } const remotes = matchingRemoteBranches(name); try { if (remotes.length > 0) { const base = await selectFromList(remotes, "找到远端分支:", true); addWorktree(name, dir, base ?? undefined); } else { console.log("未找到远端分支,创建新分支"); addWorktree(name, dir); } console.log(`工作区已创建于 ${dir}`); } catch (e) { console.error(`错误: ${e.message}`); process.exit(1); } } async function handleWithoutName(worktreesDir) { const remotes = listRemoteBranches(); if (remotes.length === 0) { console.log("未找到远端分支"); process.exit(1); } const selected = await selectFromList(remotes, "远端分支列表:", true); if (selected) { const name = shortBranchName(selected); const dir = resolve(worktreesDir, name); try { addWorktreeSafe(name, dir, selected); } catch (e) { console.error(`错误: ${e.message}`); process.exit(1); } } else { const name = await inputBranchName(); const dir = resolve(worktreesDir, name); try { addWorktreeSafe(name, dir); } catch (e) { console.error(`错误: ${e.message}`); process.exit(1); } } } process.on("SIGINT", () => process.exit(1)); async function main() { const branchName = process.argv[2]; const rootDir = getRootDir(); const worktreesDir = resolve(rootDir, ".worktrees"); mkdirSync(worktreesDir, { recursive: true }); console.log("正在从远端获取最新分支信息..."); fetchRemote(); if (branchName) { await handleWithName(branchName, worktreesDir); } else { await handleWithoutName(worktreesDir); } } main().catch((e) => { console.error(`错误: ${e.message}`); process.exit(1); });