diff --git a/.src/utils/diff-utils.ts b/.src/utils/diff-utils.ts new file mode 100644 index 0000000..3f1ce88 --- /dev/null +++ b/.src/utils/diff-utils.ts @@ -0,0 +1,282 @@ +import * as fs from 'fs-extra'; +import * as path from 'path'; +import { DiffResult, DiffFile, DiffLine, FileChange } from '../types/index'; +export { parseDiffOutput } from './git-utils'; + +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('---'); + 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('```'); + lines.push(''); + } + + return lines.join('\n'); +} + +export function formatDiffAsJson(diffResult: DiffResult): string { + return JSON.stringify(diffResult, null, 2); +} + +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('---'); + 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(''); + } + + 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 { + 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)); + 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 parts.length > 0 ? parts.join(' ') : 'No changes'; +} + +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++; + } + } + + 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; +}