diff --git a/bin/init-dev-branch.js b/bin/init-dev-branch.js new file mode 100644 index 0000000..8bed64c --- /dev/null +++ b/bin/init-dev-branch.js @@ -0,0 +1,243 @@ +#!/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); +});