#!/usr/bin/env node const { spawnSync } = require('child_process'); const prompts = require('prompts'); 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; } } class PromptAbortedError extends Error { constructor() { super('操作已取消'); this.name = 'PromptAbortedError'; } } 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 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'); } async function promptValue(config) { const response = await prompts(config, { onCancel() { throw new PromptAbortedError(); } }); return response[config.name]; } async function selectOption(title, options) { log('', 'reset'); return promptValue({ type: 'select', name: 'value', message: title, hint: '使用上下方向键选择,回车确认', choices: options.map(option => ({ title: option.label, value: option.value, description: option.description })) }); } async function selectBranch(title, branches, currentBranch) { return selectOption( title, branches.map(branchName => ({ value: branchName, label: branchName === currentBranch ? `${branchName} (当前分支)` : branchName })) ); } async function confirm(message) { log('', 'reset'); return promptValue({ type: 'confirm', name: 'confirmed', message, initial: true }); } async function runSingleBranchMerge(state) { const branches = getLocalBranches(); if (branches.length < 2) { throw new Error('本地分支数量不足,无法执行单分支合并'); } const sourceBranch = await selectBranch('请选择来源分支', branches, state.currentBranch); const targetCandidates = branches.filter(branchName => branchName !== sourceBranch); const targetBranch = await selectBranch('请选择目标分支', targetCandidates, state.currentBranch); log(`\n将执行: ${sourceBranch} -> ${targetBranch}\n`, 'cyan'); const accepted = await confirm('确认开始合并吗'); 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(state) { if (state.currentBranch === 'production') { throw new Error('当前分支为 production,不能执行部署合并'); } log(`\n将执行部署合并: ${state.currentBranch} -> master -> production\n`, 'cyan'); const accepted = await confirm('确认开始部署合并吗'); 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 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('请选择操作类型', [ { label: '单分支合并', value: 'single', description: '手动选择来源分支和目标分支' }, { label: '部署合并', value: 'deploy', description: '将当前分支依次合并到 master 和 production' } ]); if (action === 'single') { await runSingleBranchMerge(state); } else { await runDeployMerge(state); } } catch (error) { if (error instanceof MergeConflictError) { printConflictGuidance(error.branchName); } else if (error instanceof PromptAbortedError) { log('\n已取消操作\n', 'yellow'); } else { log(`\n错误: ${error.message}\n`, 'red'); } process.exitCode = error instanceof PromptAbortedError ? 0 : 1; } finally { if (state.shouldRestoreWorkspace) { restoreWorkspace(state); } } } run();