feat: Python 版 init-dev-branch 改写为 Node.js 脚本
This commit is contained in:
243
bin/init-dev-branch.js
Normal file
243
bin/init-dev-branch.js
Normal file
@@ -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);
|
||||
});
|
||||
Reference in New Issue
Block a user