Files
ai-pull-request/app/lib/git.test.js

115 lines
4.3 KiB
JavaScript

import { test } from 'node:test';
import assert from 'node:assert/strict';
import { mkdtempSync, writeFileSync, rmSync, readFileSync } from 'node:fs';
import { tmpdir } from 'node:os';
import { join } from 'node:path';
import { execFileSync } from 'node:child_process';
import { Git } from './git.js';
function g(cwd, args) {
return execFileSync('git', args, { cwd, encoding: 'utf8' });
}
/** 建立 bare remote + working clone,於 target/source 製造會衝突的變更。 */
function setupRepo() {
const root = mkdtempSync(join(tmpdir(), 'git-test-'));
const bare = join(root, 'remote.git');
const work = join(root, 'work');
execFileSync('git', ['init', '-q', '--bare', bare]);
execFileSync('git', ['clone', '-q', bare, work]);
g(work, ['config', 'user.email', 'test@example.com']);
g(work, ['config', 'user.name', 'tester']);
g(work, ['config', 'commit.gpgsign', 'false']);
writeFileSync(join(work, 'file.txt'), 'base\n');
g(work, ['add', '-A']);
g(work, ['commit', '-q', '-m', 'base']);
const def = g(work, ['rev-parse', '--abbrev-ref', 'HEAD']).trim();
g(work, ['checkout', '-q', '-b', 'target']);
writeFileSync(join(work, 'file.txt'), 'target change\n');
g(work, ['commit', '-qam', 'target change']);
g(work, ['checkout', '-q', def]);
g(work, ['checkout', '-q', '-b', 'source']);
writeFileSync(join(work, 'file.txt'), 'source change\n');
g(work, ['commit', '-qam', 'source change']);
g(work, ['push', '-q', 'origin', 'target', 'source', def]);
return { root, bare, work };
}
test('detectConflict: 偵測到衝突並回傳衝突檔,事後還原暫存分支', () => {
const { root, bare, work } = setupRepo();
try {
const git = new Git({ cwd: work, remoteUrl: bare, token: 'x' });
git.configure();
git.fetchBranches(['target', 'source']);
assert.ok(git.countAheadCommits('target', 'source') >= 1);
const det = git.detectConflict('target', 'source');
assert.equal(det.hasConflict, true);
assert.ok(det.files.includes('file.txt'));
// 暫存分支應已清除
assert.ok(!g(work, ['branch']).includes('__conflict_check'));
} finally {
rmSync(root, { recursive: true, force: true });
}
});
test('createResolveBranch: 推送含衝突標記的解衝突分支到 remote', () => {
const { root, bare, work } = setupRepo();
try {
const git = new Git({ cwd: work, remoteUrl: bare, token: 'x' });
git.configure();
git.fetchBranches(['target', 'source']);
const res = git.createResolveBranch({ target: 'target', source: 'source', resolveBranch: 'resolve-x' });
assert.ok(res.files.includes('file.txt'));
// remote 應有 resolve-x 分支
assert.ok(execFileSync('git', ['--git-dir', bare, 'branch'], { encoding: 'utf8' }).includes('resolve-x'));
// 已 commit 的檔案應保留衝突標記
assert.ok(readFileSync(join(work, 'file.txt'), 'utf8').includes('<<<<<<<'));
} finally {
rmSync(root, { recursive: true, force: true });
}
});
test('detectConflict: 可順利合併時回報無衝突', () => {
const root = mkdtempSync(join(tmpdir(), 'git-test-'));
try {
const bare = join(root, 'remote.git');
const work = join(root, 'work');
execFileSync('git', ['init', '-q', '--bare', bare]);
execFileSync('git', ['clone', '-q', bare, work]);
g(work, ['config', 'user.email', 'test@example.com']);
g(work, ['config', 'user.name', 'tester']);
g(work, ['config', 'commit.gpgsign', 'false']);
writeFileSync(join(work, 'a.txt'), 'base\n');
g(work, ['add', '-A']);
g(work, ['commit', '-qm', 'base']);
const def = g(work, ['rev-parse', '--abbrev-ref', 'HEAD']).trim();
g(work, ['checkout', '-q', '-b', 'target']); // target 不動
g(work, ['checkout', '-q', def]);
g(work, ['checkout', '-q', '-b', 'source']);
writeFileSync(join(work, 'b.txt'), 'new file\n'); // 改不同檔,無衝突
g(work, ['add', '-A']);
g(work, ['commit', '-qm', 'add b']);
g(work, ['push', '-q', 'origin', 'target', 'source', def]);
const git = new Git({ cwd: work, remoteUrl: bare, token: 'x' });
git.configure();
git.fetchBranches(['target', 'source']);
const det = git.detectConflict('target', 'source');
assert.equal(det.hasConflict, false);
assert.deepEqual(det.files, []);
} finally {
rmSync(root, { recursive: true, force: true });
}
});