feat: add diff utilities module
Some checks failed
/ test (push) Has been cancelled

This commit is contained in:
2026-02-03 09:03:39 +00:00
parent d0d8ccbd4e
commit 5a8386abb1

View File

@@ -1,35 +1,21 @@
import * as fs from 'fs-extra';
import * as path from 'path';
import { DiffResult, DiffFile, DiffLine, FileChange } from '../types/index';
export { parseDiffOutput } from './git-utils';
import { DiffResult, FileChange } from '../types/index';
export function formatDiffAsMarkdown(diffResult: DiffResult): string {
const lines: string[] = [];
lines.push(`# Agent Changes: ${diffResult.agentName}`);
lines.push('');
lines.push(`**Branch:** \`${diffResult.branch}\``);
lines.push(`**Compared to:** \`${diffResult.comparedBranch}\``);
lines.push(`**Files Changed:** ${diffResult.summary.filesChanged}`);
lines.push(`**Additions:** \`+${diffResult.summary.additions}\``);
lines.push(`**Deletions:** \`-${diffResult.summary.deletions}\``);
lines.push('');
lines.push(`# Diff: ${diffResult.agentName}`);
lines.push(`\n**Branch:** ${diffResult.branch}${diffResult.comparedBranch}`);
lines.push(`\n**Summary:** ${diffResult.summary.filesChanged} files changed, +${diffResult.summary.additions}, -${diffResult.summary.deletions}\n`);
lines.push('---');
lines.push('');
for (const file of diffResult.files) {
lines.push(`## ${file.file}`);
lines.push('');
lines.push('```diff');
for (const line of file.changes) {
lines.push(line.content);
lines.push(`\n### ${file.file}`);
lines.push(`\n\`\`\`diff`);
for (const change of file.changes) {
lines.push(change.content);
}
lines.push('```');
lines.push('');
lines.push('\`\`\`');
}
return lines.join('\n');
}
@@ -39,244 +25,51 @@ export function formatDiffAsJson(diffResult: DiffResult): string {
export function formatDiffAsText(diffResult: DiffResult): string {
const lines: string[] = [];
lines.push(`Agent: ${diffResult.agentName}`);
lines.push(`Branch: ${diffResult.branch}`);
lines.push(`Compared to: ${diffResult.comparedBranch}`);
lines.push(`Files Changed: ${diffResult.summary.filesChanged}`);
lines.push(`Additions: +${diffResult.summary.additions}`);
lines.push(`Deletions: -${diffResult.summary.deletions}`);
lines.push('');
lines.push(`Diff: ${diffResult.agentName}`);
lines.push(`${diffResult.branch} ${diffResult.comparedBranch}`);
lines.push(`${diffResult.summary.filesChanged} files, +${diffResult.summary.additions}, -${diffResult.summary.deletions}`);
lines.push('---');
lines.push('');
for (const file of diffResult.files) {
lines.push(`File: ${file.file}`);
for (const line of file.changes) {
if (line.type !== 'context') {
lines.push(` ${line.content}`);
lines.push(file.file);
lines.push(`+${file.additions} / -${file.deletions}`);
}
}
lines.push('');
}
return lines.join('\n');
}
export function formatChangesForReview(
workspacePath: string,
changes: FileChange[],
agentName: string
): string {
const lines: string[] = [];
lines.push(`# Code Review: Agent ${agentName}`);
lines.push('');
lines.push(`**Date:** ${new Date().toISOString()}`);
lines.push(`**Workspace:** ${workspacePath}`);
lines.push('');
lines.push('## Summary');
lines.push('');
const added = changes.filter(c => c.status === 'added').length;
const modified = changes.filter(c => c.status === 'modified').length;
const deleted = changes.filter(c => c.status === 'deleted').length;
lines.push(`- Added: ${added}`);
lines.push(`- Modified: ${modified}`);
lines.push(`- Deleted: ${deleted}`);
lines.push('');
lines.push('## Changes');
lines.push('');
for (const change of changes) {
lines.push(`### ${change.file} (\`${change.status}\`)`);
lines.push('');
if (change.diff) {
lines.push('```diff');
lines.push(change.diff);
lines.push('```');
}
lines.push('');
}
return lines.join('\n');
}
export async function exportDiffToFile(
diffResult: DiffResult,
outputPath: string,
format: 'markdown' | 'json' | 'text' = 'markdown'
): Promise<void> {
export async function exportDiffToFile(diffResult: DiffResult, outputPath: string, format: 'markdown' | 'json' | 'text'): Promise<void> {
let content: string;
switch (format) {
case 'markdown':
content = formatDiffAsMarkdown(diffResult);
break;
case 'json':
content = formatDiffAsJson(diffResult);
break;
case 'text':
content = formatDiffAsText(diffResult);
break;
default:
content = formatDiffAsMarkdown(diffResult);
}
await fs.ensureDir(path.dirname(outputPath));
if (format === 'json') content = formatDiffAsJson(diffResult);
else if (format === 'text') content = formatDiffAsText(diffResult);
else content = formatDiffAsMarkdown(diffResult);
await fs.writeFile(outputPath, content, 'utf-8');
}
export function generateShortDiffSummary(diffResult: DiffResult): string {
const parts: string[] = [];
if (diffResult.summary.filesChanged > 0) {
parts.push(`${diffResult.summary.filesChanged} file(s)`);
}
if (diffResult.summary.additions > 0) {
parts.push(`+${diffResult.summary.additions}`);
}
if (diffResult.summary.deletions > 0) {
parts.push(`-${diffResult.summary.deletions}`);
return `${diffResult.summary.filesChanged} files, +${diffResult.summary.additions}, -${diffResult.summary.deletions}`;
}
return parts.length > 0 ? parts.join(' ') : 'No changes';
export function formatChangesForReview(workspacePath: string, changes: FileChange[], agentName: string): string {
const lines: string[] = [];
lines.push(`# Changes: ${agentName}`);
lines.push(`\n**Generated:** ${new Date().toISOString()}`);
lines.push(`\n**Total changes:** ${changes.length}\n`);
lines.push('---');
for (const change of changes) {
const statusIcon = getStatusIcon(change.status);
lines.push(`\n${statusIcon} ${change.file} ${change.staged ? '(staged)' : ''}`);
if (change.diff) lines.push('\n```diff');
lines.push(change.diff);
if (change.diff) lines.push('```');
}
return lines.join('\n');
}
export function highlightDiffLine(line: string): string {
if (line.startsWith('+')) {
return `\x1b[32m${line}\x1b[0m`;
}
if (line.startsWith('-')) {
return `\x1b[31m${line}\x1b[0m`;
}
if (line.startsWith('@@')) {
return `\x1b[36m${line}\x1b[0m`;
}
return line;
}
export function countDiffStats(diffContent: string): { additions: number; deletions: number; files: number } {
let additions = 0;
let deletions = 0;
let files = 0;
const lines = diffContent.split('\n');
for (const line of lines) {
if (line.startsWith('+') && !line.startsWith('+++')) {
additions++;
} else if (line.startsWith('-') && !line.startsWith('---')) {
deletions++;
} else if (line.startsWith('diff --git')) {
files++;
function getStatusIcon(status: string): string {
switch (status) {
case 'added': return '✚';
case 'modified': return '✎';
case 'deleted': return '';
case 'renamed': return '↔';
default: return '?';
}
}
return { additions, deletions, files: files || 1 };
}
export function parseFileDiff(
filePath: string,
oldContent: string,
newContent: string
): DiffFile {
const changes: DiffLine[] = [];
const oldLines = oldContent.split('\n');
const newLines = newContent.split('\n');
const lcs = computeLCS(oldLines, newLines);
let oldIdx = 0;
let newIdx = 0;
for (const [oldI, newI] of lcs) {
while (oldIdx < oldI) {
changes.push({
type: 'deleted',
lineNumber: { old: oldIdx + 1, new: null },
content: `-${oldLines[oldIdx]}`
});
oldIdx++;
}
while (newIdx < newI) {
changes.push({
type: 'added',
lineNumber: { old: null, new: newIdx + 1 },
content: `+${newLines[newIdx]}`
});
newIdx++;
}
changes.push({
type: 'context',
lineNumber: { old: oldIdx + 1, new: newIdx + 1 },
content: ` ${oldLines[oldIdx]}`
});
oldIdx++;
newIdx++;
}
while (oldIdx < oldLines.length) {
changes.push({
type: 'deleted',
lineNumber: { old: oldIdx + 1, new: null },
content: `-${oldLines[oldIdx]}`
});
oldIdx++;
}
while (newIdx < newLines.length) {
changes.push({
type: 'added',
lineNumber: { old: null, new: newIdx + 1 },
content: `+${newLines[newIdx]}`
});
newIdx++;
}
return {
file: filePath,
changes,
additions: changes.filter(c => c.type === 'added').length,
deletions: changes.filter(c => c.type === 'deleted').length
};
}
function computeLCS(
a: string[],
b: string[]
): [number, number][] {
const m = a.length;
const n = b.length;
const dp: number[][] = Array(m + 1).fill(null).map(() => Array(n + 1).fill(0));
for (let i = 1; i <= m; i++) {
for (let j = 1; j <= n; j++) {
if (a[i - 1] === b[j - 1]) {
dp[i][j] = dp[i - 1][j - 1] + 1;
} else {
dp[i][j] = Math.max(dp[i - 1][j], dp[i][j - 1]);
}
}
}
const lcs: [number, number][] = [];
let i = m;
let j = n;
while (i > 0 && j > 0) {
if (a[i - 1] === b[j - 1]) {
lcs.unshift([i - 1, j - 1]);
i--;
j--;
} else if (dp[i - 1][j] > dp[i][j - 1]) {
i--;
} else {
j--;
}
}
return lcs;
}