背景
- 对于某些通过命令生成的配置文件(如 TypeScript 类型文件等),需要上传,但不需要做冲突检测
实现步骤
- 专门维护一个分支 B 用于上传不需要检测的文件 .gitignore 不需要忽略,然后在其它分支配置 .gitignore 忽略不需要检测的文件
- 每次运行命令后,在 B 分支生成最新文件后提交到 B 分支远程,然后再自动切换回 A 分支,这样文件就会保存在本地且不会被冲突检测,不影响其它分支提交
- 在新的开发启动项目时,运行命令就能够将远程 B 分支维护的文件放在本地,从而正常运行
const fs = require('fs');
const fsex = require('fs-extra');
const chalk = require('chalk');
const globAll = require('glob-all');
const path = require('path');
const { spawn, exec } = require('child_process');
const Hook = {
beforeGernerate: 'beforeGernerate',
afterGernerate: 'afterGernerate',
};
const command = 'npx';
const outputDir = path.relative(process.cwd(), './src/api_backup');
const copyDir = path.relative(process.cwd(), './src/api');
const cacheDir = path.relative(process.cwd(), './stale');
let originalBranch = '';
const targetBranch = 'feature/only_types';
const args = [
'openapi-typescript-codegen',
'--input',
'https://xxxx',
'--output',
cacheDir,
'--client',
'node',
'--exportCore',
false,
'--exportServices',
false,
'--exportModels',
true,
];
const tipTimer = setTimeout(() => {
console.log(
`${chalk.red(
'Network loading failed or did not log in to Swagger documentation. Please'
)} ${chalk.yellow('1. Exit the process')} ${chalk.yellow(
'2. Log in to the Swagger document'
)} ${chalk.yellow('3. try run "npm install" again')} ${chalk.yellow(
'4.try run "npm run type" again'
)}`
);
}, 20000);
fetchSwagger();
async function fetchSwagger() {
console.log(chalk.green('Command executing'));
checkoutBranch();
const process = spawn(command, args, { stdio: 'inherit' });
process.on('close', (code) => {
if (code === 0) {
ensureAndMakeDir();
incrementalUpdate(cacheDir, outputDir);
} else {
console.log(chalk.red(`Command failed with code ${code}`));
}
clearTimeout(tipTimer);
});
}
async function incrementalUpdate(latestFolder, staleFolder) {
console.log(chalk.green('Incremental updating ...'));
const cacheFiles = globAll.sync(latestFolder + '/**/*') || [];
try {
const promises = cacheFiles.map((file) => {
const relativePath = path.relative(latestFolder, file);
const file1 = path.join(latestFolder, relativePath);
const file2 = path.join(staleFolder, relativePath);
return new Promise((resolve, reject) => {
fs.stat(file1, (err, stats) => {
if (err) return reject(err);
if (stats.isFile()) {
if (fs.existsSync(file2)) {
compareAndUpdate(file1, file2)
.then((result) => {
resolve(result);
})
.catch((err) => reject(err));
} else {
fs.copyFile(file1, file2, (err) => {
if (err) return reject(err);
resolve(`Added ${file} to ${staleFolder}`);
});
}
} else {
resolve();
}
});
});
});
return await Promise.all(promises)
.then((results) => {
results.forEach((result) => {
if (result) console.log(result);
});
rmCacheDir();
copyOutPutToApi();
})
.catch((err) => {
rmCacheDir();
console.log(chalk.red(`Incremental update failed! ${err}`));
});
} catch (error) {
rmCacheDir();
console.log(chalk.red(`Incremental update failed! ${error}`));
}
}
function compareAndUpdate(file1, file2) {
return new Promise((resolve, reject) => {
fs.readFile(file1, 'utf8', (err, data1) => {
if (err) return reject(`Updated ${file2} with content from ${file1} failed!`);
fs.readFile(file2, 'utf8', (err, data2) => {
if (err) return reject(`Updated ${file2} with content from ${file1} failed!`);
const content1 = data1.replace(/\s/g, '');
const content2 = data2.replace(/\s/g, '');
if (content1 !== content2) {
fs.copyFile(file1, file2, (err) => {
if (err) return reject(`Updated ${file2} with content from ${file1} failed!`);
resolve(`Updated ${file2} with content from ${file1}`);
});
} else {
resolve();
}
});
});
});
}
function rmCacheDir() {
if (fs.existsSync(cacheDir)) {
fs.rm(cacheDir, { recursive: true, force: true }, (err) => {
if (err) {
console.error(err.message);
return;
}
console.log(chalk.green(`Remove stale successfully!`));
});
}
}
function rewriteExportToIndex() {
const latestFiles = globAll.sync(outputDir + '/models/**/*') || [];
let str = '';
latestFiles.forEach((item) => {
const filename = path.basename(item, '.ts');
str += `export type { ${filename} } from './models/${filename}'; \n`;
});
fs.writeFile(outputDir + '/index.ts', str, (err) => {
if (err) {
console.error('write failed:', err);
return;
}
});
fs.writeFile(copyDir + '/index.ts', str, (err) => {
if (err) {
console.error('write failed:', err);
return;
}
commitAndResetBranch();
console.log(chalk.green(`Incremental update successfully!`));
});
}
function ensureAndMakeDir() {
if (!fs.existsSync(outputDir)) {
console.log(chalk.yellow("src/api_backup/models/ directory does not exist! let's create it"));
fs.mkdirSync(outputDir + '/models', { recursive: true });
}
if (!fs.existsSync(copyDir)) {
console.log(chalk.yellow("src/api/models/ directory does not exist! let's create it"));
fs.mkdirSync(outputDir + '/models', { recursive: true });
}
}
async function copyOutPutToApi() {
try {
await fsex.copy(outputDir, copyDir);
rewriteExportToIndex();
} catch (error) {
console.log(chalk.red('copy output dir error:', error));
rmCacheDir();
}
}
function execAsync(command, hookName) {
return new Promise((resolve, reject) => {
exec(command, (error, stdout, stderr) => {
if (error) {
if (stderr.includes('CONFLICT')) {
console.log(
chalk.red(
'A conflict occurs when pulling a remote branch. Please manually resolve it first!'
)
);
reject(new Error('Git conflict'));
} else {
if (hookName === Hook.afterGernerate) {
if (
stdout.includes('无文件要提交') ||
stdout.includes('nothing to commit') ||
stdout.includes('working tree clean') ||
stdout.includes('clean workspace')
) {
resolve({ stdout, stderr });
return;
}
}
reject(error);
}
console.log(chalk.red('exec command error:', error, stderr, stdout));
if (hookName === Hook.beforeGernerate) {
process.exit(1);
}
return;
}
resolve({ stdout, stderr });
});
});
}
async function checkoutBranch() {
try {
const { stdout: currentBranch } = await execAsync(
'git branch --show-current',
Hook.beforeGernerate
);
originalBranch = currentBranch.trim();
console.log(`Current branch is: ${originalBranch}`);
const { stdout: status } = await execAsync('git status --porcelain', Hook.beforeGernerate);
if (status.trim()) {
console.log(
chalk.yellow('There are currently uncommitted changes, please commit them first!')
);
process.exit(1);
return;
}
console.log('Pull origin branch');
const { stdout: pullLog } = await execAsync('git pull origin', Hook.beforeGernerate);
console.log(pullLog);
await execAsync(`git checkout ${targetBranch}`, Hook.beforeGernerate);
console.log(chalk.yellow(`Now current branch is: ${targetBranch}`));
} catch (error) {
console.log(chalk.red(`Something Wrong! Reset Branch!`));
await execAsync(`git checkout ${originalBranch}`);
process.exit(1);
}
}
async function commitAndResetBranch() {
try {
const commands = [
`git add ${outputDir}`,
`git commit -m "feat: auto commit types" --no-verify`,
`git push origin ${targetBranch}`,
];
try {
console.log(chalk.green(`auto add、commit、push!`));
await execAsync(commands.join(' && '), Hook.afterGernerate);
console.log(chalk.green(`auto add、commit、push successfully!`));
} catch (error) {
console.log(chalk.red('auto commit error', error));
}
await execAsync(`git checkout ${originalBranch}`, Hook.afterGernerate);
console.log(chalk.yellow(`Now current branch is: ${originalBranch}`));
} catch (error) {
console.log(chalk.red(`Something Wrong when commit! Reset Branch!`, error));
await execAsync(`git checkout -- ${outputDir}`, Hook.afterGernerate);
await execAsync(`git checkout ${originalBranch}`, Hook.afterGernerate);
rmCacheDir();
}
}