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

334 lines
8.9 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 { 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();