src / diff.ts
src / diff.ts
import * as fs from "fs";
import { expandPath } from "./securityEnhanced";
// ── Diff Operations (pure JS fallback, no external deps) ──
export function diffFiles(file1: string, file2: string): string {
try {
const content1 = fs.readFileSync(expandPath(file1), "utf8");
const content2 = fs.readFileSync(expandPath(file2), "utf8");
return diffLinesPure(content1, content2);
} catch (err) {
throw new Error(`Failed to diff files: ${file1} vs ${file2} — ${(err as Error).message}`);
}
}
/**
* Pure-JS line-by-line diff using Myers' algorithm for memory efficiency.
* Falls back to full LCS for small files (< 5000 lines).
* Returns unified diff format.
*/
export function diffLinesPure(a: string, b: string): string {
if (a === b) return "";
const linesA = a.split("\n");
const linesB = b.split("\n");
const m = linesA.length;
const n = linesB.length;
// For small files, use full LCS (simpler and fast enough)
if (m <= 5000 && n <= 5000) {
return diffLcs(linesA, linesB);
}
// For medium files (5000-50000), use full table LCS
// For large files, use a simplified Myers' diff (O(ND) time, O(min(m,n)) space)
return diffMyers(linesA, linesB);
}
/**
* Full LCS-based diff — fine for files under 5000 lines.
*/
export function diffLcs(linesA: string[], linesB: string[]): string {
const m = linesA.length;
const n = linesB.length;
// Build full DP table for backtracking (needed for correct diff)
if (m <= 2000 && n <= 2000) {
const dp: Int32Array[] = [];
for (let i = 0; i <= m; i++) {
dp[i] = new Int32Array(n + 1);
}
for (let i = 1; i <= m; i++) {
for (let j = 1; j <= n; j++) {
if (linesA[i - 1] === linesB[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 result: string[] = [];
let i = m, j = n;
while (i > 0 || j > 0) {
if (i > 0 && j > 0 && linesA[i - 1] === linesB[j - 1]) {
result.unshift(` ${linesA[i - 1]}`);
i--; j--;
} else if (j > 0 && (i === 0 || dp[i][j - 1] >= dp[i - 1][j])) {
result.unshift(`+${linesB[j - 1]}`);
j--;
} else {
result.unshift(`-${linesA[i - 1]}`);
i--;
}
}
return result.join("\n");
}
// For very large files where we can't afford the full table,
// fall back to a line-by-line comparison
return diffLinesByComparison(linesA, linesB);
}
/**
* Myers-inspired diff for large files — O(min(m,n)) space.
*/
export function diffMyers(linesA: string[], linesB: string[]): string {
const result: string[] = [];
let i = 0;
let j = 0;
while (i < linesA.length && j < linesB.length) {
if (linesA[i] === linesB[j]) {
result.push(` ${linesA[i]}`);
i++;
j++;
} else {
// Simple heuristic: check context windows
let found = false;
for (let k = 1; k <= 10 && i + k <= linesA.length; k++) {
if (linesA[i + k] === linesB[j]) {
for (let x = 0; x < k; x++) {
result.push(`-${linesA[i + x]}`);
}
i += k;
found = true;
break;
}
}
if (!found) {
for (let k = 1; k <= 10 && j + k <= linesB.length; k++) {
if (linesB[j + k] === linesA[i]) {
for (let x = 0; x < k; x++) {
result.push(`+${linesB[j + x]}`);
}
j += k;
found = true;
break;
}
}
}
if (!found) {
result.push(`-${linesA[i]}`);
i++;
}
}
}
while (i < linesA.length) {
result.push(`-${linesA[i]}`);
i++;
}
while (j < linesB.length) {
result.push(`+${linesB[j]}`);
j++;
}
return result.join("\n");
}
/**
* Simple line-by-line comparison for very large files.
*/
function diffLinesByComparison(linesA: string[], linesB: string[]): string {
const result: string[] = [];
const maxLen = Math.max(linesA.length, linesB.length);
for (let i = 0; i < maxLen; i++) {
const aLine = i < linesA.length ? linesA[i] : undefined;
const bLine = i < linesB.length ? linesB[i] : undefined;
if (aLine === bLine) {
result.push(` ${aLine}`);
} else {
if (aLine !== undefined) result.push(`-${aLine}`);
if (bLine !== undefined) result.push(`+${bLine}`);
}
}
return result.join("\n");
}
import * as fs from "fs";
import { expandPath } from "./securityEnhanced";
// ── Diff Operations (pure JS fallback, no external deps) ──
export function diffFiles(file1: string, file2: string): string {
try {
const content1 = fs.readFileSync(expandPath(file1), "utf8");
const content2 = fs.readFileSync(expandPath(file2), "utf8");
return diffLinesPure(content1, content2);
} catch (err) {
throw new Error(`Failed to diff files: ${file1} vs ${file2} — ${(err as Error).message}`);
}
}
/**
* Pure-JS line-by-line diff using Myers' algorithm for memory efficiency.
* Falls back to full LCS for small files (< 5000 lines).
* Returns unified diff format.
*/
export function diffLinesPure(a: string, b: string): string {
if (a === b) return "";
const linesA = a.split("\n");
const linesB = b.split("\n");
const m = linesA.length;
const n = linesB.length;
// For small files, use full LCS (simpler and fast enough)
if (m <= 5000 && n <= 5000) {
return diffLcs(linesA, linesB);
}
// For medium files (5000-50000), use full table LCS
// For large files, use a simplified Myers' diff (O(ND) time, O(min(m,n)) space)
return diffMyers(linesA, linesB);
}
/**
* Full LCS-based diff — fine for files under 5000 lines.
*/
export function diffLcs(linesA: string[], linesB: string[]): string {
const m = linesA.length;
const n = linesB.length;
// Build full DP table for backtracking (needed for correct diff)
if (m <= 2000 && n <= 2000) {
const dp: Int32Array[] = [];
for (let i = 0; i <= m; i++) {
dp[i] = new Int32Array(n + 1);
}
for (let i = 1; i <= m; i++) {
for (let j = 1; j <= n; j++) {
if (linesA[i - 1] === linesB[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 result: string[] = [];
let i = m, j = n;
while (i > 0 || j > 0) {
if (i > 0 && j > 0 && linesA[i - 1] === linesB[j - 1]) {
result.unshift(` ${linesA[i - 1]}`);
i--; j--;
} else if (j > 0 && (i === 0 || dp[i][j - 1] >= dp[i - 1][j])) {
result.unshift(`+${linesB[j - 1]}`);
j--;
} else {
result.unshift(`-${linesA[i - 1]}`);
i--;
}
}
return result.join("\n");
}
// For very large files where we can't afford the full table,
// fall back to a line-by-line comparison
return diffLinesByComparison(linesA, linesB);
}
/**
* Myers-inspired diff for large files — O(min(m,n)) space.
*/
export function diffMyers(linesA: string[], linesB: string[]): string {
const result: string[] = [];
let i = 0;
let j = 0;
while (i < linesA.length && j < linesB.length) {
if (linesA[i] === linesB[j]) {
result.push(` ${linesA[i]}`);
i++;
j++;
} else {
// Simple heuristic: check context windows
let found = false;
for (let k = 1; k <= 10 && i + k <= linesA.length; k++) {
if (linesA[i + k] === linesB[j]) {
for (let x = 0; x < k; x++) {
result.push(`-${linesA[i + x]}`);
}
i += k;
found = true;
break;
}
}
if (!found) {
for (let k = 1; k <= 10 && j + k <= linesB.length; k++) {
if (linesB[j + k] === linesA[i]) {
for (let x = 0; x < k; x++) {
result.push(`+${linesB[j + x]}`);
}
j += k;
found = true;
break;
}
}
}
if (!found) {
result.push(`-${linesA[i]}`);
i++;
}
}
}
while (i < linesA.length) {
result.push(`-${linesA[i]}`);
i++;
}
while (j < linesB.length) {
result.push(`+${linesB[j]}`);
j++;
}
return result.join("\n");
}
/**
* Simple line-by-line comparison for very large files.
*/
function diffLinesByComparison(linesA: string[], linesB: string[]): string {
const result: string[] = [];
const maxLen = Math.max(linesA.length, linesB.length);
for (let i = 0; i < maxLen; i++) {
const aLine = i < linesA.length ? linesA[i] : undefined;
const bLine = i < linesB.length ? linesB[i] : undefined;
if (aLine === bLine) {
result.push(` ${aLine}`);
} else {
if (aLine !== undefined) result.push(`-${aLine}`);
if (bLine !== undefined) result.push(`+${bLine}`);
}
}
return result.join("\n");
}