|
|
|
@ -1,7 +1,7 @@
|
|
|
|
#!/usr/bin/env node
|
|
|
|
#!/usr/bin/env node
|
|
|
|
|
|
|
|
|
|
|
|
const readline = require('readline');
|
|
|
|
|
|
|
|
const { spawnSync } = require('child_process');
|
|
|
|
const { spawnSync } = require('child_process');
|
|
|
|
|
|
|
|
const prompts = require('prompts');
|
|
|
|
|
|
|
|
|
|
|
|
const COLORS = {
|
|
|
|
const COLORS = {
|
|
|
|
red: '\x1b[31m',
|
|
|
|
red: '\x1b[31m',
|
|
|
|
@ -19,6 +19,13 @@ class MergeConflictError extends Error {
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class PromptAbortedError extends Error {
|
|
|
|
|
|
|
|
constructor() {
|
|
|
|
|
|
|
|
super('操作已取消');
|
|
|
|
|
|
|
|
this.name = 'PromptAbortedError';
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function log(message, color = 'reset') {
|
|
|
|
function log(message, color = 'reset') {
|
|
|
|
console.log(`${COLORS[color]}${message}${COLORS.reset}`);
|
|
|
|
console.log(`${COLORS[color]}${message}${COLORS.reset}`);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
@ -53,24 +60,6 @@ function git(args, options = {}) {
|
|
|
|
return runCommand('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() {
|
|
|
|
function getCurrentBranch() {
|
|
|
|
return git(['branch', '--show-current'], { silent: true }).trim();
|
|
|
|
return git(['branch', '--show-current'], { silent: true }).trim();
|
|
|
|
}
|
|
|
|
}
|
|
|
|
@ -178,68 +167,67 @@ function mergeBranch(sourceBranch, targetBranch, state) {
|
|
|
|
log('合并成功\n', 'green');
|
|
|
|
log('合并成功\n', 'green');
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function printMenu(title, options) {
|
|
|
|
async function promptValue(config) {
|
|
|
|
log(`\n${title}`, 'cyan');
|
|
|
|
const response = await prompts(config, {
|
|
|
|
options.forEach((option, index) => {
|
|
|
|
onCancel() {
|
|
|
|
console.log(` ${index + 1}. ${option.label}`);
|
|
|
|
throw new PromptAbortedError();
|
|
|
|
});
|
|
|
|
|
|
|
|
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');
|
|
|
|
return response[config.name];
|
|
|
|
}
|
|
|
|
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
async function selectBranch(prompt, title, branches, currentBranch) {
|
|
|
|
async function selectOption(title, options) {
|
|
|
|
const options = branches.map(branchName => ({
|
|
|
|
log('', 'reset');
|
|
|
|
value: branchName,
|
|
|
|
|
|
|
|
label: branchName === currentBranch ? `${branchName} (当前分支)` : branchName
|
|
|
|
return promptValue({
|
|
|
|
}));
|
|
|
|
type: 'select',
|
|
|
|
|
|
|
|
name: 'value',
|
|
|
|
const selected = await selectOption(prompt, title, options);
|
|
|
|
message: title,
|
|
|
|
return selected.value;
|
|
|
|
hint: '使用上下方向键选择,回车确认',
|
|
|
|
|
|
|
|
choices: options.map(option => ({
|
|
|
|
|
|
|
|
title: option.label,
|
|
|
|
|
|
|
|
value: option.value,
|
|
|
|
|
|
|
|
description: option.description
|
|
|
|
|
|
|
|
}))
|
|
|
|
|
|
|
|
});
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
async function confirm(prompt, message) {
|
|
|
|
async function selectBranch(title, branches, currentBranch) {
|
|
|
|
while (true) {
|
|
|
|
return selectOption(
|
|
|
|
const answer = (await prompt.ask(`${message} (y/n): `)).toLowerCase();
|
|
|
|
title,
|
|
|
|
|
|
|
|
branches.map(branchName => ({
|
|
|
|
if (answer === 'y' || answer === 'yes') {
|
|
|
|
value: branchName,
|
|
|
|
return true;
|
|
|
|
label: branchName === currentBranch ? `${branchName} (当前分支)` : branchName
|
|
|
|
}
|
|
|
|
}))
|
|
|
|
|
|
|
|
);
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if (answer === 'n' || answer === 'no') {
|
|
|
|
async function confirm(message) {
|
|
|
|
return false;
|
|
|
|
log('', 'reset');
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
log('请输入 y 或 n。\n', 'red');
|
|
|
|
return promptValue({
|
|
|
|
}
|
|
|
|
type: 'confirm',
|
|
|
|
|
|
|
|
name: 'confirmed',
|
|
|
|
|
|
|
|
message,
|
|
|
|
|
|
|
|
initial: true
|
|
|
|
|
|
|
|
});
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
async function runSingleBranchMerge(prompt, state) {
|
|
|
|
async function runSingleBranchMerge(state) {
|
|
|
|
const branches = getLocalBranches();
|
|
|
|
const branches = getLocalBranches();
|
|
|
|
|
|
|
|
|
|
|
|
if (branches.length < 2) {
|
|
|
|
if (branches.length < 2) {
|
|
|
|
throw new Error('本地分支数量不足,无法执行单分支合并');
|
|
|
|
throw new Error('本地分支数量不足,无法执行单分支合并');
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const sourceBranch = await selectBranch(prompt, '请选择来源分支', branches, state.currentBranch);
|
|
|
|
const sourceBranch = await selectBranch('请选择来源分支', branches, state.currentBranch);
|
|
|
|
const targetCandidates = branches.filter(branchName => branchName !== sourceBranch);
|
|
|
|
const targetCandidates = branches.filter(branchName => branchName !== sourceBranch);
|
|
|
|
const targetBranch = await selectBranch(prompt, '请选择目标分支', targetCandidates, state.currentBranch);
|
|
|
|
const targetBranch = await selectBranch('请选择目标分支', targetCandidates, state.currentBranch);
|
|
|
|
|
|
|
|
|
|
|
|
log(`\n将执行: ${sourceBranch} -> ${targetBranch}\n`, 'cyan');
|
|
|
|
log(`\n将执行: ${sourceBranch} -> ${targetBranch}\n`, 'cyan');
|
|
|
|
|
|
|
|
|
|
|
|
const accepted = await confirm(prompt, '确认开始合并吗');
|
|
|
|
const accepted = await confirm('确认开始合并吗');
|
|
|
|
if (!accepted) {
|
|
|
|
if (!accepted) {
|
|
|
|
log('已取消操作', 'yellow');
|
|
|
|
log('已取消操作', 'yellow');
|
|
|
|
return;
|
|
|
|
return;
|
|
|
|
@ -257,14 +245,14 @@ async function runSingleBranchMerge(prompt, state) {
|
|
|
|
log('========================================\n', 'green');
|
|
|
|
log('========================================\n', 'green');
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
async function runDeployMerge(prompt, state) {
|
|
|
|
async function runDeployMerge(state) {
|
|
|
|
if (state.currentBranch === 'production') {
|
|
|
|
if (state.currentBranch === 'production') {
|
|
|
|
throw new Error('当前分支为 production,不能执行部署合并');
|
|
|
|
throw new Error('当前分支为 production,不能执行部署合并');
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
log(`\n将执行部署合并: ${state.currentBranch} -> master -> production\n`, 'cyan');
|
|
|
|
log(`\n将执行部署合并: ${state.currentBranch} -> master -> production\n`, 'cyan');
|
|
|
|
|
|
|
|
|
|
|
|
const accepted = await confirm(prompt, '确认开始部署合并吗');
|
|
|
|
const accepted = await confirm('确认开始部署合并吗');
|
|
|
|
if (!accepted) {
|
|
|
|
if (!accepted) {
|
|
|
|
log('已取消操作', 'yellow');
|
|
|
|
log('已取消操作', 'yellow');
|
|
|
|
return;
|
|
|
|
return;
|
|
|
|
@ -295,7 +283,6 @@ async function runDeployMerge(prompt, state) {
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
async function run() {
|
|
|
|
async function run() {
|
|
|
|
const prompt = createPrompt();
|
|
|
|
|
|
|
|
const state = {
|
|
|
|
const state = {
|
|
|
|
currentBranch: '',
|
|
|
|
currentBranch: '',
|
|
|
|
hasChanges: false,
|
|
|
|
hasChanges: false,
|
|
|
|
@ -316,27 +303,27 @@ async function run() {
|
|
|
|
|
|
|
|
|
|
|
|
log(`当前分支: ${state.currentBranch}\n`, 'cyan');
|
|
|
|
log(`当前分支: ${state.currentBranch}\n`, 'cyan');
|
|
|
|
|
|
|
|
|
|
|
|
const action = await selectOption(prompt, '请选择操作类型', [
|
|
|
|
const action = await selectOption('请选择操作类型', [
|
|
|
|
{ label: '单分支合并', value: 'single' },
|
|
|
|
{ label: '单分支合并', value: 'single', description: '手动选择来源分支和目标分支' },
|
|
|
|
{ label: '部署合并(当前分支 -> master -> production)', value: 'deploy' }
|
|
|
|
{ label: '部署合并', value: 'deploy', description: '将当前分支依次合并到 master 和 production' }
|
|
|
|
]);
|
|
|
|
]);
|
|
|
|
|
|
|
|
|
|
|
|
if (action.value === 'single') {
|
|
|
|
if (action === 'single') {
|
|
|
|
await runSingleBranchMerge(prompt, state);
|
|
|
|
await runSingleBranchMerge(state);
|
|
|
|
} else {
|
|
|
|
} else {
|
|
|
|
await runDeployMerge(prompt, state);
|
|
|
|
await runDeployMerge(state);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
} catch (error) {
|
|
|
|
} catch (error) {
|
|
|
|
if (error instanceof MergeConflictError) {
|
|
|
|
if (error instanceof MergeConflictError) {
|
|
|
|
printConflictGuidance(error.branchName);
|
|
|
|
printConflictGuidance(error.branchName);
|
|
|
|
|
|
|
|
} else if (error instanceof PromptAbortedError) {
|
|
|
|
|
|
|
|
log('\n已取消操作\n', 'yellow');
|
|
|
|
} else {
|
|
|
|
} else {
|
|
|
|
log(`\n错误: ${error.message}\n`, 'red');
|
|
|
|
log(`\n错误: ${error.message}\n`, 'red');
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
process.exitCode = 1;
|
|
|
|
process.exitCode = error instanceof PromptAbortedError ? 0 : 1;
|
|
|
|
} finally {
|
|
|
|
} finally {
|
|
|
|
prompt.close();
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
if (state.shouldRestoreWorkspace) {
|
|
|
|
if (state.shouldRestoreWorkspace) {
|
|
|
|
restoreWorkspace(state);
|
|
|
|
restoreWorkspace(state);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|