100 lines
5.4 KiB
JavaScript
100 lines
5.4 KiB
JavaScript
import assert from 'node:assert/strict';
|
|
import { execFileSync, spawnSync } from 'node:child_process';
|
|
import { mkdtempSync, readFileSync, rmSync, writeFileSync } from 'node:fs';
|
|
import { tmpdir } from 'node:os';
|
|
import { join } from 'node:path';
|
|
import test from 'node:test';
|
|
import { fileURLToPath } from 'node:url';
|
|
import { DEFAULT_REPOSITORIES, createReport, inspectRepository, renderMarkdown } from './report-unmerged-branches.mjs';
|
|
|
|
function fixture(t) {
|
|
const path = mkdtempSync(join(tmpdir(), 'easyai-branch-report-'));
|
|
t.after(() => rmSync(path, { recursive: true, force: true }));
|
|
const git = (...args) => execFileSync('git', args, { cwd: path, encoding: 'utf8', stdio: ['pipe', 'pipe', 'pipe'] }).trim();
|
|
git('init', '-b', 'master');
|
|
git('config', 'user.name', '分支验收');
|
|
git('config', 'user.email', 'branch-test@example.invalid');
|
|
const commit = (file, contents, subject) => {
|
|
writeFileSync(join(path, file), contents); git('add', file); git('commit', '-m', subject);
|
|
return git('rev-parse', 'HEAD');
|
|
};
|
|
const base = commit('base.txt', 'base\n', 'chore: 基线');
|
|
git('checkout', '-b', 'equivalent');
|
|
const original = commit('equivalent.txt', 'integrated\n', 'fix: 原始修复');
|
|
git('checkout', 'master'); git('cherry-pick', '--no-commit', original); git('commit', '-m', 'fix: 等价集成修复');
|
|
const target = git('rev-parse', 'HEAD');
|
|
assert.notEqual(original, target);
|
|
git('update-ref', 'refs/remotes/origin/master', target);
|
|
git('update-ref', 'refs/remotes/origin/equivalent', original);
|
|
git('update-ref', 'refs/remotes/origin/merged', base);
|
|
git('symbolic-ref', 'refs/remotes/origin/HEAD', 'refs/remotes/origin/master');
|
|
git('checkout', '-b', 'feature');
|
|
const unmerged = commit('feature.txt', 'new\n', 'feat: 未合入能力');
|
|
git('update-ref', 'refs/remotes/origin/feature', unmerged);
|
|
return { path, git, original, target, unmerged, repository: { name: 'web', label: '前端', path, targetBranch: 'master' } };
|
|
}
|
|
|
|
test('默认前端发布和分支检查均以 master 为主分支', () => {
|
|
assert.equal(DEFAULT_REPOSITORIES.find(x => x.name === 'web').targetBranch, 'master');
|
|
});
|
|
|
|
test('报告真实未合入提交,排除已合入历史和 cherry-pick 等价补丁,且不改工作区', t => {
|
|
const f = fixture(t);
|
|
writeFileSync(join(f.path, 'base.txt'), '用户未提交编辑\n');
|
|
const before = f.git('status', '--porcelain');
|
|
const result = inspectRepository(f.repository);
|
|
assert.equal(result.status, 'complete');
|
|
assert.equal(result.summary.checkedBranches, 3);
|
|
assert.equal(result.summary.uniqueUnmergedCommits, 1);
|
|
assert.equal(result.branches.find(x => x.name === 'feature').unmergedCommits[0].sha, f.unmerged);
|
|
assert.equal(result.branches.find(x => x.name === 'equivalent').status, 'equivalent-integrated');
|
|
assert.equal(result.branches.find(x => x.name === 'equivalent').equivalentCommits[0].sha, f.original);
|
|
assert.equal(result.branches.find(x => x.name === 'merged').status, 'merged');
|
|
assert.equal(f.git('status', '--porcelain'), before);
|
|
assert.equal(f.git('rev-parse', 'HEAD'), f.unmerged);
|
|
});
|
|
|
|
test('相同提交位于多个分支时,仓库提交总数去重并保留各分支', t => {
|
|
const f = fixture(t);
|
|
f.git('update-ref', 'refs/remotes/origin/feature-copy', f.unmerged);
|
|
const result = inspectRepository(f.repository);
|
|
assert.equal(result.summary.branchesNeedingReview, 2);
|
|
assert.equal(result.summary.uniqueUnmergedCommits, 1);
|
|
});
|
|
|
|
test('仅合并提交尚未进入主分支也标为待核对,不冒充普通功能提交', t => {
|
|
const f = fixture(t);
|
|
const tree = f.git('rev-parse', f.target + '^{tree}');
|
|
const merge = f.git('commit-tree', tree, '-p', f.target, '-p', f.original, '-m', 'chore: 合并历史待核对');
|
|
f.git('update-ref', 'refs/remotes/origin/integration', merge);
|
|
const result = inspectRepository(f.repository);
|
|
const branch = result.branches.find(x => x.name === 'integration');
|
|
assert.equal(branch.status, 'needs-review');
|
|
assert.equal(branch.unmergedCommits.length, 0);
|
|
assert.equal(branch.mergeCommits[0].sha, merge);
|
|
assert.match(renderMarkdown(createReport([f.repository])), /合并提交.*合并历史待核对/);
|
|
});
|
|
|
|
test('主分支缺失时明确检查失败,CLI 保存证据且返回非零', t => {
|
|
const f = fixture(t);
|
|
const repository = { ...f.repository, targetBranch: 'missing' };
|
|
const result = createReport([repository]);
|
|
assert.equal(result.status, 'incomplete');
|
|
assert.equal(result.repositories[0].errors.length, 1);
|
|
assert.doesNotMatch(renderMarkdown(result), /其他远端分支无未合入改动/);
|
|
const config = join(f.path, 'repos.json'), output = join(f.path, 'report');
|
|
writeFileSync(config, JSON.stringify([repository]));
|
|
const cli = spawnSync(process.execPath, [fileURLToPath(new URL('./report-unmerged-branches.mjs', import.meta.url)), '--config', config, '--output-dir', output], { encoding: 'utf8' });
|
|
assert.equal(cli.status, 1);
|
|
assert.equal(JSON.parse(readFileSync(join(output, 'unmerged-branches.json'), 'utf8')).status, 'incomplete');
|
|
assert.match(readFileSync(join(output, 'unmerged-branches.md'), 'utf8'), /检查失败/);
|
|
});
|
|
|
|
test('fetch 失败不能把旧 refs 当成本日检查成功', t => {
|
|
const f = fixture(t);
|
|
const result = inspectRepository(f.repository, { fetch: true });
|
|
assert.equal(result.status, 'incomplete');
|
|
assert.equal(result.summary.checkedBranches, 0);
|
|
assert.match(result.errors[0].message, /origin/);
|
|
});
|