You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
flow-playform-react/scripts/merge-to-production.js

347 lines
9.2 KiB
JavaScript

This file contains ambiguous Unicode characters!

This file contains ambiguous Unicode characters that may be confused with others in your current locale. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to highlight these characters.

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