* 飞虹 Code (Muse Code 参照复刻)
* 晋江市飞虹智科技企业管理有限公司 · 飞扬企源研发中心 · 负责人:吴赐虹
*
* Git 辅助(M3 diff/rollback):仅作用于会话 touchedFiles,绝不整仓回滚,避免误删。
* 非 git 仓库时安全退出并提示,不执行任何破坏性操作。
*/
import { spawn } from 'child_process';
import { existsSync, rmSync } from 'fs';
import { join } from 'path';
import { platform } from 'os';
const NULL_DEVICE = platform() === 'win32' ? 'nul' : '/dev/null';
interface GitOut {
code: number;
out: string;
err: string;
}
function git(args: string[], cwd: string): Promise<GitOut> {
return new Promise((resolve) => {
const child = spawn('git', args, { cwd, shell: false });
let out = '';
let err = '';
child.stdout?.on('data', (d) => (out += d.toString()));
child.stderr?.on('data', (d) => (err += d.toString()));
child.on('close', (code) => resolve({ code: code ?? 1, out, err }));
child.on('error', (e) => resolve({ code: 1, out, err: e.message }));
});
}
export function isGitRepo(cwd: string): boolean {
return existsSync(join(cwd, '.git'));
}
export async function gitDiff(cwd: string, files?: string[]): Promise<string> {
if (!isGitRepo(cwd)) {
return '(当前目录不是 git 仓库,无法生成 diff;请手动核对 touchedFiles)';
}
const parts: string[] = [];
const target = files && files.length ? files : null;
const trackedArgs = ['diff', '--color=never'];
if (target) trackedArgs.push('--', ...target);
const tracked = await git(trackedArgs, cwd);
if (tracked.out.trim()) parts.push(tracked.out);
const status = await git(['status', '--porcelain', '--'], cwd);
const untracked = status.out
.split('\n')
.map((l) => l.trim())
.filter(Boolean)
.filter((l) => l.startsWith('??'))
.map((l) => l.slice(2).trim());
const untrackedToShow = target ? untracked.filter((f) => target.includes(f)) : untracked;
for (const f of untrackedToShow) {
const abs = join(cwd, f);
if (!existsSync(abs)) continue;
const ni = await git(['diff', '--no-index', '--color=never', NULL_DEVICE, abs.replace(/\\/g, '/')], cwd);
parts.push(ni.out || `(新增文件: ${f})`);
}
return parts.join('\n').trim() || '(无变更)';
}
export interface RollbackResult {
reverted: string[];
removed: string[];
errors: string[];
}
* 回滚会话 touchedFiles:
* - 已跟踪文件 → git checkout -- <file>
* - 未跟踪文件 → 删除
* 未确认(--yes)或非 git 仓库时拒绝执行,绝不静默破坏。
*/
export async function gitRollback(
cwd: string,
files: string[],
opts: { yes?: boolean } = {},
): Promise<RollbackResult> {
const res: RollbackResult = { reverted: [], removed: [], errors: [] };
if (!isGitRepo(cwd)) {
res.errors.push('当前目录不是 git 仓库,回滚已中止(避免误删)');
return res;
}
if (!opts.yes) {
res.errors.push('回滚是破坏性操作,请加 --yes 显式确认');
return res;
}
const status = await git(['status', '--porcelain', '--'], cwd);
const untracked = new Set(
status.out
.split('\n')
.map((l) => l.trim())
.filter(Boolean)
.filter((l) => l.startsWith('??'))
.map((l) => l.slice(2).trim()),
);
for (const f of files) {
if (untracked.has(f)) {
try {
rmSync(join(cwd, f), { force: true, recursive: true });
res.removed.push(f);
} catch (e) {
res.errors.push(`${f}: ${e instanceof Error ? e.message : String(e)}`);
}
} else {
const r = await git(['checkout', '--', f], cwd);
if (r.code === 0) res.reverted.push(f);
else res.errors.push(`${f}: ${r.err.trim() || 'checkout 失败'}`);
}
}
return res;
}