Files
Alfred/bin/init-dev-branch.js

244 lines
5.8 KiB
JavaScript

#!/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);
});