mirror of
https://github.com/Silentely/eSIM-Tools.git
synced 2026-09-02 22:14:02 +08:00
🔧 chore(hooks): 新增提交与推送质量门禁
This commit is contained in:
3
.githooks/commit-msg
Executable file
3
.githooks/commit-msg
Executable file
@@ -0,0 +1,3 @@
|
||||
#!/usr/bin/env sh
|
||||
|
||||
node scripts/commit-msg-lint.js "$1"
|
||||
3
.githooks/pre-commit
Executable file
3
.githooks/pre-commit
Executable file
@@ -0,0 +1,3 @@
|
||||
#!/usr/bin/env sh
|
||||
|
||||
node scripts/pre-commit-check.js
|
||||
3
.githooks/pre-push
Executable file
3
.githooks/pre-push
Executable file
@@ -0,0 +1,3 @@
|
||||
#!/usr/bin/env sh
|
||||
|
||||
node scripts/pre-push-check.js --mode=fast
|
||||
3
.githooks/prepare-commit-msg
Executable file
3
.githooks/prepare-commit-msg
Executable file
@@ -0,0 +1,3 @@
|
||||
#!/usr/bin/env sh
|
||||
|
||||
node scripts/prepare-commit-msg.js "$1" "$2" "$3"
|
||||
29
README.md
29
README.md
@@ -112,10 +112,10 @@
|
||||
```bash
|
||||
# Windows
|
||||
start_simyo_server.bat
|
||||
|
||||
|
||||
# macOS/Linux
|
||||
./start_simyo_server.sh
|
||||
|
||||
|
||||
# 或手动启动
|
||||
npm start
|
||||
```
|
||||
@@ -269,7 +269,7 @@ open tests/test_simyo_esim.html
|
||||
### 参与方式
|
||||
1. 🍴 Fork项目仓库
|
||||
2. 🌿 创建功能分支 (`git checkout -b feature/AmazingFeature`)
|
||||
3. 💾 提交更改 (`git commit -m 'Add AmazingFeature'`)
|
||||
3. 💾 按规范提交 (`git commit -m '✨ feat(core): 新增二维码缓存逻辑'`)
|
||||
4. 📤 推送分支 (`git push origin feature/AmazingFeature`)
|
||||
5. 🔃 创建Pull Request
|
||||
|
||||
@@ -278,6 +278,29 @@ open tests/test_simyo_esim.html
|
||||
- 添加必要的测试用例
|
||||
- 更新相关文档
|
||||
|
||||
### 提交信息规范(emoji + 中文)
|
||||
1. 启用仓库钩子:`npm run hooks:install`
|
||||
2. 提交格式:`<emoji> <type>(optional-scope): <中文描述>`
|
||||
3. type 可选值:`feat`、`fix`、`docs`、`style`、`refactor`、`perf`、`test`、`build`、`ci`、`chore`、`revert`
|
||||
4. emoji 与 type 需要匹配(例如 `✨` 对应 `feat`,`🐛` 对应 `fix`)
|
||||
|
||||
示例:
|
||||
- `✨ feat(auth): 新增登录态自动续期`
|
||||
- `🐛 fix(simyo): 修复验证码重试逻辑`
|
||||
- `📝 docs(readme): 更新安装说明`
|
||||
|
||||
### 当前 Git 钩子
|
||||
- `pre-commit`:提交前格式化 staged 文件,并执行 JS/JSON 语法检查与智能引号拦截
|
||||
- `pre-push`:默认快速模式(`pre-commit` + 相关测试),无差异文件时回退全量测试
|
||||
- `prepare-commit-msg`:提交信息为空时自动填充模板
|
||||
- `commit-msg`:校验提交信息格式(emoji + type + 中文描述 + 映射关系)
|
||||
|
||||
可用命令:
|
||||
- 查看钩子列表:`npm run hooks:list`
|
||||
- 手动执行提交前检查:`npm run precommit:check`
|
||||
- 手动执行推送前全量检查:`npm run prepush:check`
|
||||
- 手动执行推送前快速检查:`npm run prepush:check:fast`
|
||||
|
||||
## 📞 支持与反馈
|
||||
|
||||
遇到问题或有建议?
|
||||
|
||||
29
README_EN.md
29
README_EN.md
@@ -100,10 +100,10 @@ Modern eSIM management toolkit designed for Giffgaff and Simyo users, supporting
|
||||
```bash
|
||||
# Windows
|
||||
start_simyo_server.bat
|
||||
|
||||
|
||||
# macOS/Linux
|
||||
./start_simyo_server.sh
|
||||
|
||||
|
||||
# Or manually start
|
||||
npm start
|
||||
```
|
||||
@@ -257,7 +257,7 @@ Contributions and suggestions welcome!
|
||||
### How to Participate
|
||||
1. 🍴 Fork project repository
|
||||
2. 🌿 Create feature branch (`git checkout -b feature/AmazingFeature`)
|
||||
3. 💾 Commit changes (`git commit -m 'Add AmazingFeature'`)
|
||||
3. 💾 Commit with format (`git commit -m '✨ feat(core): 新增二维码缓存逻辑'`)
|
||||
4. 📤 Push branch (`git push origin feature/AmazingFeature`)
|
||||
5. 🔃 Create Pull Request
|
||||
|
||||
@@ -266,6 +266,29 @@ Contributions and suggestions welcome!
|
||||
- Add necessary test cases
|
||||
- Update relevant documentation
|
||||
|
||||
### Commit Message Rules (emoji + Chinese summary)
|
||||
1. Enable repo hooks: `npm run hooks:install`
|
||||
2. Format: `<emoji> <type>(optional-scope): <中文描述>`
|
||||
3. Allowed types: `feat`, `fix`, `docs`, `style`, `refactor`, `perf`, `test`, `build`, `ci`, `chore`, `revert`
|
||||
4. Emoji must match type (for example, `✨` for `feat`, `🐛` for `fix`)
|
||||
|
||||
Examples:
|
||||
- `✨ feat(auth): 新增登录态自动续期`
|
||||
- `🐛 fix(simyo): 修复验证码重试逻辑`
|
||||
- `📝 docs(readme): 更新安装说明`
|
||||
|
||||
### Current Git Hooks
|
||||
- `pre-commit`: formats staged files and runs JS/JSON syntax + smart quotes checks
|
||||
- `pre-push`: defaults to fast mode (`pre-commit` + related tests), falls back to full tests when no diff files are detected
|
||||
- `prepare-commit-msg`: auto-fills a template when commit message is empty
|
||||
- `commit-msg`: validates message format (emoji + type + Chinese summary + mapping)
|
||||
|
||||
Useful commands:
|
||||
- List hooks: `npm run hooks:list`
|
||||
- Run pre-commit checks manually: `npm run precommit:check`
|
||||
- Run full pre-push checks manually: `npm run prepush:check`
|
||||
- Run fast pre-push checks manually: `npm run prepush:check:fast`
|
||||
|
||||
## 📞 Support and Feedback
|
||||
|
||||
Having issues or suggestions?
|
||||
|
||||
@@ -4,6 +4,12 @@
|
||||
"description": "专为Giffgaff和Simyo用户设计的eSIM管理工具集",
|
||||
"main": "index.js",
|
||||
"scripts": {
|
||||
"hooks:install": "git config core.hooksPath .githooks",
|
||||
"hooks:list": "ls -1 .githooks",
|
||||
"precommit:check": "node scripts/pre-commit-check.js",
|
||||
"prepush:check": "node scripts/pre-push-check.js --mode=full",
|
||||
"prepush:check:fast": "node scripts/pre-push-check.js --mode=fast",
|
||||
"commitmsg:lint": "node scripts/commit-msg-lint.js",
|
||||
"prestart": "npm run build",
|
||||
"predev": "npm run build",
|
||||
"prenetlify-dev": "npm run build",
|
||||
|
||||
165
scripts/commit-msg-lint.js
Normal file
165
scripts/commit-msg-lint.js
Normal file
@@ -0,0 +1,165 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
/**
|
||||
* commit-msg 校验:
|
||||
* 1. 首段必须为 emoji(或 :shortcode:);
|
||||
* 2. 后续遵循 Conventional Commits 头部格式;
|
||||
* 3. emoji 与 type 必须匹配;
|
||||
* 4. 描述必须包含中文字符。
|
||||
*
|
||||
* 格式:<emoji> <type>(optional-scope): <中文描述>
|
||||
* 示例:✨ feat(auth): 新增登录态自动续期
|
||||
*/
|
||||
|
||||
const fs = require('fs');
|
||||
|
||||
const COMMIT_TYPES = [
|
||||
'feat',
|
||||
'fix',
|
||||
'docs',
|
||||
'style',
|
||||
'refactor',
|
||||
'perf',
|
||||
'test',
|
||||
'build',
|
||||
'ci',
|
||||
'chore',
|
||||
'revert'
|
||||
];
|
||||
|
||||
const TYPE_TO_EMOJI = {
|
||||
feat: ['✨', ':sparkles:'],
|
||||
fix: ['🐛', ':bug:'],
|
||||
docs: ['📝', ':memo:'],
|
||||
style: ['💄', ':lipstick:'],
|
||||
refactor: ['♻️', '♻', ':recycle:'],
|
||||
perf: ['⚡️', '⚡', ':zap:'],
|
||||
test: ['✅', ':white_check_mark:'],
|
||||
build: ['📦', ':package:'],
|
||||
ci: ['👷', ':construction_worker:'],
|
||||
chore: ['🔧', '🧹', ':wrench:', ':broom:'],
|
||||
revert: ['⏪️', '⏪', ':rewind:']
|
||||
};
|
||||
|
||||
const TOKEN_TO_TYPES = new Map();
|
||||
for (const [type, tokens] of Object.entries(TYPE_TO_EMOJI)) {
|
||||
for (const token of tokens) {
|
||||
const key = token.startsWith(':')
|
||||
? token.toLowerCase()
|
||||
: token.replace(/\uFE0F/g, '');
|
||||
if (!TOKEN_TO_TYPES.has(key)) {
|
||||
TOKEN_TO_TYPES.set(key, []);
|
||||
}
|
||||
TOKEN_TO_TYPES.get(key).push(type);
|
||||
}
|
||||
}
|
||||
|
||||
const HEADER_PATTERN = new RegExp(
|
||||
`^(${COMMIT_TYPES.join('|')})(\\([a-z0-9._/-]+\\))?:\\s(.+)$`,
|
||||
'u'
|
||||
);
|
||||
|
||||
const ALLOWED_PAIRS_HELP = Object.entries(TYPE_TO_EMOJI)
|
||||
.map(([type, tokens]) => `${type}: ${tokens.join(' / ')}`)
|
||||
.join('\n ');
|
||||
|
||||
function fail(message) {
|
||||
console.error('\n❌ 提交信息格式不符合规范:');
|
||||
console.error(` ${message}\n`);
|
||||
console.error('期望格式:');
|
||||
console.error(' <emoji> <type>(optional-scope): <中文描述>');
|
||||
console.error('\nemoji 与 type 对应关系:');
|
||||
console.error(` ${ALLOWED_PAIRS_HELP}`);
|
||||
console.error('\n示例:');
|
||||
console.error(' ✨ feat(auth): 新增登录态自动续期');
|
||||
console.error(' 🐛 fix(simyo): 修复验证码重试逻辑');
|
||||
console.error(' 📝 docs(readme): 更新安装说明\n');
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
function getFirstMeaningfulLine(content) {
|
||||
const lines = content.split(/\r?\n/);
|
||||
for (const line of lines) {
|
||||
const trimmed = line.trim();
|
||||
if (!trimmed || trimmed.startsWith('#')) {
|
||||
continue;
|
||||
}
|
||||
return trimmed;
|
||||
}
|
||||
return '';
|
||||
}
|
||||
|
||||
function isEmojiToken(token) {
|
||||
const key = token.startsWith(':')
|
||||
? token.toLowerCase()
|
||||
: token.replace(/\uFE0F/g, '');
|
||||
return TOKEN_TO_TYPES.has(key);
|
||||
}
|
||||
|
||||
function validateEmojiTypePair(emojiToken, type) {
|
||||
const key = emojiToken.startsWith(':')
|
||||
? emojiToken.toLowerCase()
|
||||
: emojiToken.replace(/\uFE0F/g, '');
|
||||
const allowedTypes = TOKEN_TO_TYPES.get(key);
|
||||
|
||||
if (!allowedTypes || allowedTypes.length === 0) {
|
||||
fail(`不支持的 emoji:${emojiToken}`);
|
||||
}
|
||||
|
||||
if (!allowedTypes.includes(type)) {
|
||||
fail(`emoji ${emojiToken} 与 type ${type} 不匹配。`);
|
||||
}
|
||||
}
|
||||
|
||||
function validateCommitHeader(header) {
|
||||
if (!header) {
|
||||
fail('提交标题为空。');
|
||||
}
|
||||
|
||||
// 允许 git 自动生成的合并/回滚信息
|
||||
if (header.startsWith('Merge ') || header.startsWith('Revert "')) {
|
||||
return;
|
||||
}
|
||||
|
||||
const firstSpaceIndex = header.indexOf(' ');
|
||||
if (firstSpaceIndex <= 0) {
|
||||
fail('缺少 emoji 前缀,或未使用空格分隔。');
|
||||
}
|
||||
|
||||
const emojiToken = header.slice(0, firstSpaceIndex).trim();
|
||||
const conventionalHeader = header.slice(firstSpaceIndex + 1).trim();
|
||||
|
||||
if (!isEmojiToken(emojiToken)) {
|
||||
fail('提交标题需要以受支持的 emoji(或 :shortcode:)开头。');
|
||||
}
|
||||
|
||||
const matched = conventionalHeader.match(HEADER_PATTERN);
|
||||
if (!matched) {
|
||||
fail(
|
||||
`类型段不合法。请使用 ${COMMIT_TYPES.join(
|
||||
', '
|
||||
)} 之一,并遵循 type(scope): subject 格式。`
|
||||
);
|
||||
}
|
||||
|
||||
const type = matched[1];
|
||||
validateEmojiTypePair(emojiToken, type);
|
||||
|
||||
const subject = matched[3].trim();
|
||||
if (!/[\u3400-\u9FFF]/u.test(subject)) {
|
||||
fail('提交描述必须包含中文字符。');
|
||||
}
|
||||
}
|
||||
|
||||
function main() {
|
||||
const messageFilePath = process.argv[2];
|
||||
if (!messageFilePath) {
|
||||
fail('未接收到 commit message 文件路径。');
|
||||
}
|
||||
|
||||
const content = fs.readFileSync(messageFilePath, 'utf8');
|
||||
const header = getFirstMeaningfulLine(content);
|
||||
validateCommitHeader(header);
|
||||
}
|
||||
|
||||
main();
|
||||
189
scripts/pre-commit-check.js
Normal file
189
scripts/pre-commit-check.js
Normal file
@@ -0,0 +1,189 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
/**
|
||||
* pre-commit 检查:
|
||||
* 1) 仅处理 staged 文件,安全格式化(LF、行尾空白、文件尾换行);
|
||||
* 2) 检查 staged JS 语法;
|
||||
* 3) 检查 staged JSON 语法;
|
||||
* 4) 拦截代码文件中的智能引号,避免引号边界问题。
|
||||
*/
|
||||
|
||||
const fs = require('fs');
|
||||
const os = require('os');
|
||||
const path = require('path');
|
||||
const { execFileSync, spawnSync } = require('child_process');
|
||||
let esbuild = null;
|
||||
try {
|
||||
esbuild = require('esbuild');
|
||||
} catch (_) {
|
||||
esbuild = null;
|
||||
}
|
||||
|
||||
const TEXT_EXTENSIONS = new Set([
|
||||
'.js',
|
||||
'.mjs',
|
||||
'.cjs',
|
||||
'.json',
|
||||
'.md',
|
||||
'.css',
|
||||
'.html',
|
||||
'.yml',
|
||||
'.yaml',
|
||||
'.sh'
|
||||
]);
|
||||
|
||||
const JS_EXTENSIONS = new Set(['.js', '.mjs', '.cjs']);
|
||||
const SMART_QUOTES_RE = /[\u2018\u2019\u201C\u201D]/u;
|
||||
|
||||
function git(args, options = {}) {
|
||||
return execFileSync('git', args, {
|
||||
encoding: 'utf8',
|
||||
stdio: ['ignore', 'pipe', 'pipe'],
|
||||
...options
|
||||
});
|
||||
}
|
||||
|
||||
function getStagedFiles() {
|
||||
const output = git([
|
||||
'diff',
|
||||
'--cached',
|
||||
'--name-only',
|
||||
'--diff-filter=ACMR',
|
||||
'-z'
|
||||
]);
|
||||
return output.split('\0').filter(Boolean);
|
||||
}
|
||||
|
||||
function shouldFormat(relPath) {
|
||||
const ext = path.extname(relPath).toLowerCase();
|
||||
if (TEXT_EXTENSIONS.has(ext)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
const baseName = path.basename(relPath);
|
||||
return baseName === '.gitignore' || baseName === '.gitattributes';
|
||||
}
|
||||
|
||||
function normalizeText(content) {
|
||||
let next = content.replace(/\r\n/g, '\n');
|
||||
next = next.replace(/[ \t]+$/gm, '');
|
||||
if (next.length > 0 && !next.endsWith('\n')) {
|
||||
next += '\n';
|
||||
}
|
||||
return next;
|
||||
}
|
||||
|
||||
function formatStagedFiles(stagedFiles) {
|
||||
const updated = [];
|
||||
for (const relPath of stagedFiles) {
|
||||
if (!shouldFormat(relPath)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const absPath = path.resolve(relPath);
|
||||
if (!fs.existsSync(absPath)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const original = fs.readFileSync(absPath, 'utf8');
|
||||
const normalized = normalizeText(original);
|
||||
if (normalized !== original) {
|
||||
fs.writeFileSync(absPath, normalized, 'utf8');
|
||||
git(['add', '--', relPath]);
|
||||
updated.push(relPath);
|
||||
}
|
||||
}
|
||||
|
||||
return updated;
|
||||
}
|
||||
|
||||
function readStagedContent(relPath) {
|
||||
return git(['show', `:${relPath}`]);
|
||||
}
|
||||
|
||||
function fallbackNodeSyntaxCheck(relPath, content) {
|
||||
const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'esim-precommit-'));
|
||||
const ext = path.extname(relPath).toLowerCase() || '.js';
|
||||
const tempFile = path.join(tempDir, `check${ext}`);
|
||||
|
||||
try {
|
||||
fs.writeFileSync(tempFile, content, 'utf8');
|
||||
const result = spawnSync(process.execPath, ['--check', tempFile], {
|
||||
encoding: 'utf8'
|
||||
});
|
||||
if (result.status !== 0) {
|
||||
throw new Error((result.stderr || result.stdout || '').trim());
|
||||
}
|
||||
} finally {
|
||||
fs.rmSync(tempDir, { recursive: true, force: true });
|
||||
}
|
||||
}
|
||||
|
||||
function checkJavaScriptSyntax(relPath, content) {
|
||||
if (esbuild) {
|
||||
esbuild.transformSync(content, {
|
||||
loader: 'js',
|
||||
sourcefile: relPath,
|
||||
charset: 'utf8'
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
fallbackNodeSyntaxCheck(relPath, content);
|
||||
}
|
||||
|
||||
function runChecks(stagedFiles) {
|
||||
const failures = [];
|
||||
|
||||
for (const relPath of stagedFiles) {
|
||||
const ext = path.extname(relPath).toLowerCase();
|
||||
if (!JS_EXTENSIONS.has(ext) && ext !== '.json') {
|
||||
continue;
|
||||
}
|
||||
|
||||
const content = readStagedContent(relPath);
|
||||
if (SMART_QUOTES_RE.test(content)) {
|
||||
failures.push(
|
||||
`${relPath}: 检测到智能引号(U+201C/U+201D/U+2018/U+2019),请改为标准半角引号。`
|
||||
);
|
||||
continue;
|
||||
}
|
||||
|
||||
try {
|
||||
if (JS_EXTENSIONS.has(ext)) {
|
||||
checkJavaScriptSyntax(relPath, content);
|
||||
} else if (ext === '.json') {
|
||||
JSON.parse(content);
|
||||
}
|
||||
} catch (error) {
|
||||
failures.push(`${relPath}: ${error.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
return failures;
|
||||
}
|
||||
|
||||
function main() {
|
||||
const stagedFiles = getStagedFiles();
|
||||
if (stagedFiles.length === 0) {
|
||||
process.exit(0);
|
||||
}
|
||||
|
||||
const formatted = formatStagedFiles(stagedFiles);
|
||||
if (formatted.length > 0) {
|
||||
console.log(`✅ 已格式化并重新暂存 ${formatted.length} 个文件`);
|
||||
}
|
||||
|
||||
const failures = runChecks(stagedFiles);
|
||||
if (failures.length > 0) {
|
||||
console.error('\n❌ pre-commit 检查失败:');
|
||||
for (const failure of failures) {
|
||||
console.error(` - ${failure}`);
|
||||
}
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
console.log('✅ pre-commit 检查通过');
|
||||
}
|
||||
|
||||
main();
|
||||
143
scripts/pre-push-check.js
Normal file
143
scripts/pre-push-check.js
Normal file
@@ -0,0 +1,143 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
/**
|
||||
* pre-push 检查:
|
||||
* 1) 先执行 pre-commit 级别检查,保证待推送内容格式与基础语法正确;
|
||||
* 2) 快速模式仅执行相关测试,全量模式执行完整测试。
|
||||
*/
|
||||
|
||||
const { execFileSync, spawnSync } = require('child_process');
|
||||
|
||||
const npmCmd = process.platform === 'win32' ? 'npm.cmd' : 'npm';
|
||||
|
||||
const RELATED_EXTENSIONS = new Set(['.js', '.mjs', '.cjs', '.json']);
|
||||
|
||||
function runGit(args) {
|
||||
return execFileSync('git', args, {
|
||||
encoding: 'utf8',
|
||||
stdio: ['ignore', 'pipe', 'pipe']
|
||||
});
|
||||
}
|
||||
|
||||
function resolveMode() {
|
||||
const argMode = process.argv
|
||||
.find(arg => arg.startsWith('--mode='))
|
||||
?.split('=')[1];
|
||||
const envMode = process.env.PREPUSH_MODE;
|
||||
const mode = (argMode || envMode || 'full').toLowerCase();
|
||||
return mode === 'fast' ? 'fast' : 'full';
|
||||
}
|
||||
|
||||
function getDiffRange() {
|
||||
try {
|
||||
const upstream = runGit([
|
||||
'rev-parse',
|
||||
'--abbrev-ref',
|
||||
'--symbolic-full-name',
|
||||
'@{upstream}'
|
||||
]).trim();
|
||||
if (upstream) {
|
||||
return `${upstream}...HEAD`;
|
||||
}
|
||||
} catch (_) {
|
||||
// ignore and fallback
|
||||
}
|
||||
|
||||
try {
|
||||
runGit(['rev-parse', '--verify', 'HEAD~1']);
|
||||
return 'HEAD~1..HEAD';
|
||||
} catch (_) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
function getRelatedFiles() {
|
||||
const range = getDiffRange();
|
||||
if (!range) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const output = runGit([
|
||||
'diff',
|
||||
'--name-only',
|
||||
'--diff-filter=ACMR',
|
||||
'-z',
|
||||
range
|
||||
]);
|
||||
const files = output.split('\0').filter(Boolean);
|
||||
|
||||
return files.filter(file => {
|
||||
const lowerFile = file.toLowerCase();
|
||||
for (const ext of RELATED_EXTENSIONS) {
|
||||
if (lowerFile.endsWith(ext)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
});
|
||||
}
|
||||
|
||||
function buildSteps(mode) {
|
||||
const steps = [
|
||||
{
|
||||
name: 'pre-commit 检查',
|
||||
args: ['run', 'precommit:check']
|
||||
}
|
||||
];
|
||||
|
||||
if (mode === 'fast') {
|
||||
const relatedFiles = getRelatedFiles();
|
||||
if (relatedFiles.length > 0) {
|
||||
steps.push({
|
||||
name: `相关测试检查(${relatedFiles.length} 个文件)`,
|
||||
args: [
|
||||
'test',
|
||||
'--',
|
||||
'--bail',
|
||||
'--findRelatedTests',
|
||||
'--passWithNoTests',
|
||||
...relatedFiles
|
||||
]
|
||||
});
|
||||
} else {
|
||||
steps.push({
|
||||
name: '测试检查(未识别差异文件,回退全量测试)',
|
||||
args: ['test', '--', '--bail']
|
||||
});
|
||||
}
|
||||
return steps;
|
||||
}
|
||||
|
||||
steps.push({
|
||||
name: '全量测试检查',
|
||||
args: ['test', '--', '--bail']
|
||||
});
|
||||
|
||||
return steps;
|
||||
}
|
||||
|
||||
function runStep(step) {
|
||||
console.log(`\n🔎 执行 ${step.name}...`);
|
||||
const result = spawnSync(npmCmd, step.args, {
|
||||
stdio: 'inherit',
|
||||
env: process.env
|
||||
});
|
||||
|
||||
if (result.status !== 0) {
|
||||
console.error(`\n❌ ${step.name}未通过,已阻止 push。`);
|
||||
process.exit(result.status || 1);
|
||||
}
|
||||
}
|
||||
|
||||
function main() {
|
||||
const mode = resolveMode();
|
||||
console.log(`\n🧭 pre-push 模式: ${mode}`);
|
||||
const steps = buildSteps(mode);
|
||||
|
||||
for (const step of steps) {
|
||||
runStep(step);
|
||||
}
|
||||
console.log('\n✅ pre-push 检查全部通过');
|
||||
}
|
||||
|
||||
main();
|
||||
54
scripts/prepare-commit-msg.js
Normal file
54
scripts/prepare-commit-msg.js
Normal file
@@ -0,0 +1,54 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
/**
|
||||
* prepare-commit-msg:
|
||||
* 在未填写提交信息时,注入标准模板,降低格式输入错误概率。
|
||||
*/
|
||||
|
||||
const fs = require('fs');
|
||||
|
||||
const TEMPLATE_HEADER = '✨ feat(core): 请填写中文描述';
|
||||
const SKIP_SOURCES = new Set(['merge', 'squash', 'commit']);
|
||||
|
||||
function getFirstMeaningfulLine(content) {
|
||||
const lines = content.split(/\r?\n/);
|
||||
for (const line of lines) {
|
||||
const trimmed = line.trim();
|
||||
if (!trimmed || trimmed.startsWith('#')) {
|
||||
continue;
|
||||
}
|
||||
return trimmed;
|
||||
}
|
||||
return '';
|
||||
}
|
||||
|
||||
function main() {
|
||||
const messageFilePath = process.argv[2];
|
||||
const commitSource = process.argv[3] || '';
|
||||
|
||||
if (!messageFilePath || SKIP_SOURCES.has(commitSource)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const original = fs.readFileSync(messageFilePath, 'utf8');
|
||||
const firstLine = getFirstMeaningfulLine(original);
|
||||
if (firstLine) {
|
||||
return;
|
||||
}
|
||||
|
||||
const commentLines = original
|
||||
.split(/\r?\n/)
|
||||
.filter(line => line.trim().startsWith('#'));
|
||||
|
||||
const content = [
|
||||
TEMPLATE_HEADER,
|
||||
'',
|
||||
'# 格式: <emoji> <type>(optional-scope): <中文描述>',
|
||||
'# 示例: 🐛 fix(simyo): 修复验证码重试逻辑',
|
||||
...commentLines
|
||||
].join('\n');
|
||||
|
||||
fs.writeFileSync(messageFilePath, content, 'utf8');
|
||||
}
|
||||
|
||||
main();
|
||||
Reference in New Issue
Block a user