#!/usr/bin/env node const readline = require('readline'); const { spawnSync } = require('child_process'); const COLORS = { red: '\x1b[31m', green: '\x1b[32m', yellow: '\x1b[33m', cyan: '\x1b[36m', reset: '\x1b[0m' }; class MergeConflictError extends Error { constructor(branchName) { super(`合并到 ${branchName} 时发生冲突`); this.name = 'MergeConflictError'; this.branchName = branchName; } } function log(message, color = 'reset') { console.log(`${COLORS[color]}${message}${COLORS.reset}`); } function runCommand(command, args, options = {}) { const { silent = false, ignoreError = false } = options; const result = spawnSync(command, args, { encoding: 'utf-8', stdio: silent ? ['inherit', 'pipe', 'pipe'] : 'inherit' }); if (result.error) { throw result.error; } if (result.status !== 0) { if (ignoreError) { return result.stdout || ''; } const error = new Error(`${command} ${args.join(' ')} failed`); error.status = result.status; error.stdout = result.stdout || ''; error.stderr = result.stderr || ''; throw error; } return result.stdout || ''; } function git(args, options = {}) { return runCommand('git', args, options); } function createPrompt() { const rl = readline.createInterface({ input: process.stdin, output: process.stdout }); return { ask(question) { return new Promise(resolve => { rl.question(question, answer => resolve(answer.trim())); }); }, close() { rl.close(); } }; } function getCurrentBranch() { return git(['branch', '--show-current'], { silent: true }).trim(); } function getLocalBranches() { return git(['branch', '--format=%(refname:short)'], { silent: true }) .split(/\r?\n/) .map(item => item.trim()) .filter(Boolean); } function hasRemoteBranch(branchName) { const output = git(['rev-parse', '--verify', '--quiet', `refs/remotes/origin/${branchName}`], { silent: true, ignoreError: true }); return output.trim().length > 0; } function stashChanges(state) { const status = git(['status', '--porcelain'], { silent: true }); state.hasChanges = status.trim().length > 0; if (!state.hasChanges) { return; } log('>>> 暂存未提交的改动...', 'yellow'); git(['stash', 'push', '-m', 'auto-stash-before-merge']); state.stashCreated = true; log('已暂存\n', 'green'); } function restoreWorkspace(state) { if (state.currentBranch && getCurrentBranch() !== state.currentBranch) { log(`>>> 切换回原分支 ${state.currentBranch}...`, 'yellow'); git(['checkout', state.currentBranch], { ignoreError: true }); } if (state.stashCreated) { log('>>> 恢复暂存的改动...', 'yellow'); git(['stash', 'pop'], { ignoreError: true }); log('已恢复\n', 'green'); } } function fetchOrigin() { log('>>> 拉取远程最新信息...', 'yellow'); git(['fetch', 'origin']); log('拉取完成\n', 'green'); } function checkoutBranch(branchName) { log(`>>> 切换到 ${branchName} 分支...`, 'yellow'); git(['checkout', branchName]); log(`已切换到 ${branchName}\n`, 'green'); } function pullBranchIfNeeded(branchName) { if (!hasRemoteBranch(branchName)) { log(`>>> 本地分支 ${branchName} 未关联 origin/${branchName},跳过 pull`, 'yellow'); return; } log(`>>> 更新 ${branchName} 分支...`, 'yellow'); git(['pull', 'origin', branchName]); log(`${branchName} 已更新\n`, 'green'); } function pushBranchIfNeeded(branchName) { if (!hasRemoteBranch(branchName)) { log(`>>> 本地分支 ${branchName} 未关联 origin/${branchName},跳过 push`, 'yellow'); return; } log(`>>> 推送 ${branchName} 到远程...`, 'yellow'); git(['push', 'origin', branchName]); log(`${branchName} 推送成功\n`, 'green'); } function printConflictGuidance(branchName) { log(`\n错误: 合并到 ${branchName} 时发生冲突`, 'red'); log('请手动解决冲突后再继续', 'red'); log('\n冲突文件:', 'yellow'); git(['diff', '--name-only', '--diff-filter=U'], { ignoreError: true }); log('\n解决冲突后可继续执行:', 'yellow'); log(' git add .', 'cyan'); log(' git commit', 'cyan'); log(` git push origin ${branchName}\n`, 'cyan'); log('如需放弃本次合并:', 'yellow'); log(' git merge --abort\n', 'cyan'); } function mergeBranch(sourceBranch, targetBranch, state) { log(`>>> 合并 ${sourceBranch} -> ${targetBranch}...`, 'yellow'); try { git(['merge', sourceBranch, '--no-edit']); } catch (error) { state.shouldRestoreWorkspace = false; throw new MergeConflictError(targetBranch); } log('合并成功\n', 'green'); } function printMenu(title, options) { log(`\n${title}`, 'cyan'); options.forEach((option, index) => { console.log(` ${index + 1}. ${option.label}`); }); console.log(''); } async function selectOption(prompt, title, options) { while (true) { printMenu(title, options); const answer = await prompt.ask('请输入编号并回车: '); const index = Number(answer); if (Number.isInteger(index) && index >= 1 && index <= options.length) { return options[index - 1]; } log('输入无效,请重新选择。\n', 'red'); } } async function selectBranch(prompt, title, branches, currentBranch) { const options = branches.map(branchName => ({ value: branchName, label: branchName === currentBranch ? `${branchName} (当前分支)` : branchName })); const selected = await selectOption(prompt, title, options); return selected.value; } async function confirm(prompt, message) { while (true) { const answer = (await prompt.ask(`${message} (y/n): `)).toLowerCase(); if (answer === 'y' || answer === 'yes') { return true; } if (answer === 'n' || answer === 'no') { return false; } log('请输入 y 或 n。\n', 'red'); } } async function runSingleBranchMerge(prompt, state) { const branches = getLocalBranches(); if (branches.length < 2) { throw new Error('本地分支数量不足,无法执行单分支合并'); } const sourceBranch = await selectBranch(prompt, '请选择来源分支', branches, state.currentBranch); const targetCandidates = branches.filter(branchName => branchName !== sourceBranch); const targetBranch = await selectBranch(prompt, '请选择目标分支', targetCandidates, state.currentBranch); log(`\n将执行: ${sourceBranch} -> ${targetBranch}\n`, 'cyan'); const accepted = await confirm(prompt, '确认开始合并吗'); if (!accepted) { log('已取消操作', 'yellow'); return; } stashChanges(state); fetchOrigin(); checkoutBranch(targetBranch); pullBranchIfNeeded(targetBranch); mergeBranch(sourceBranch, targetBranch, state); pushBranchIfNeeded(targetBranch); log('\n========================================', 'green'); log(` 已完成 ${sourceBranch} -> ${targetBranch} 合并`, 'green'); log('========================================\n', 'green'); } async function runDeployMerge(prompt, state) { if (state.currentBranch === 'production') { throw new Error('当前分支为 production,不能执行部署合并'); } log(`\n将执行部署合并: ${state.currentBranch} -> master -> production\n`, 'cyan'); const accepted = await confirm(prompt, '确认开始部署合并吗'); if (!accepted) { log('已取消操作', 'yellow'); return; } stashChanges(state); fetchOrigin(); checkoutBranch('master'); pullBranchIfNeeded('master'); if (state.currentBranch !== 'master') { mergeBranch(state.currentBranch, 'master', state); } else { log('当前分支已是 master,跳过 feature -> master 合并\n', 'yellow'); } pushBranchIfNeeded('master'); checkoutBranch('production'); pullBranchIfNeeded('production'); mergeBranch('master', 'production', state); pushBranchIfNeeded('production'); log('\n========================================', 'green'); log(` 已完成 ${state.currentBranch} -> master -> production`, 'green'); log('========================================\n', 'green'); } async function run() { const prompt = createPrompt(); const state = { currentBranch: '', hasChanges: false, stashCreated: false, shouldRestoreWorkspace: true }; try { log('\n========================================', 'cyan'); log(' Git 分支合并工具', 'cyan'); log('========================================\n', 'cyan'); state.currentBranch = getCurrentBranch(); if (!state.currentBranch) { throw new Error('当前不在任何本地分支上,无法执行合并'); } log(`当前分支: ${state.currentBranch}\n`, 'cyan'); const action = await selectOption(prompt, '请选择操作类型', [ { label: '单分支合并', value: 'single' }, { label: '部署合并(当前分支 -> master -> production)', value: 'deploy' } ]); if (action.value === 'single') { await runSingleBranchMerge(prompt, state); } else { await runDeployMerge(prompt, state); } } catch (error) { if (error instanceof MergeConflictError) { printConflictGuidance(error.branchName); } else { log(`\n错误: ${error.message}\n`, 'red'); } process.exitCode = 1; } finally { prompt.close(); if (state.shouldRestoreWorkspace) { restoreWorkspace(state); } } } run();