246 lines
5.8 KiB
JavaScript
246 lines
5.8 KiB
JavaScript
#!/usr/bin/env node
|
|
/**
|
|
* 开发分支工作区初始化脚本
|
|
*
|
|
* 用于创建基于远端分支或新建的开发分支工作区。
|
|
*/
|
|
|
|
import { execFileSync } from "node:child_process";
|
|
import { existsSync, mkdirSync } from "node:fs";
|
|
import { relative, resolve } from "node:path";
|
|
import { createInterface } from "node:readline";
|
|
|
|
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));
|
|
}
|
|
|
|
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 fetchRemote() {
|
|
try {
|
|
git(["fetch", "--quiet"]);
|
|
} catch {
|
|
console.warn("警告: 无法获取远端信息,继续使用本地数据");
|
|
}
|
|
}
|
|
|
|
function getRootDir() {
|
|
try {
|
|
return resolve(git(["rev-parse", "--show-toplevel"]).trim());
|
|
} catch {
|
|
console.error("错误: 不在 git 仓库中");
|
|
process.exit(1);
|
|
}
|
|
}
|
|
|
|
function git(args, opts) {
|
|
return execFileSync("git", args, { encoding: "utf-8", stdio: "pipe", ...opts });
|
|
}
|
|
|
|
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);
|
|
}
|
|
}
|
|
}
|
|
|
|
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("错误: 分支名称不能为空");
|
|
}
|
|
}
|
|
|
|
function listRemoteBranches() {
|
|
try {
|
|
return git(["branch", "-r"])
|
|
.trim()
|
|
.split(/\r?\n/)
|
|
.map((l) => l.trim())
|
|
.filter((l) => l && !l.includes(" -> "));
|
|
} catch {
|
|
return [];
|
|
}
|
|
}
|
|
|
|
function localBranchExists(name) {
|
|
try {
|
|
git(["show-ref", "--verify", "--quiet", `refs/heads/${name}`]);
|
|
return true;
|
|
} catch {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
function matchingRemoteBranches(name) {
|
|
return listRemoteBranches().filter((l) => l.endsWith(`/${name}`));
|
|
}
|
|
|
|
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;
|
|
}
|
|
}
|
|
|
|
function shortBranchName(remoteRef) {
|
|
const idx = remoteRef.indexOf("/");
|
|
return idx === -1 ? remoteRef : remoteRef.slice(idx + 1);
|
|
}
|
|
|
|
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;
|
|
}
|
|
}
|
|
|
|
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);
|
|
});
|