Files
Codewalkers/apps/web/src/components/review/parse-diff.ts
Lukas May 5968a6ba88 feat: split FileDiff into metadata FileDiff + hunk-bearing FileDiffDetail
Prepares the review components for the backend phase that returns
metadata-only file lists from getPhaseReviewDiff. FileDiff now holds
only path/status/additions/deletions; FileDiffDetail extends it with
hunks. Renames changeType→status and adds 'binary' to the union.

Also fixes two pre-existing TypeScript errors: InitiativeReview was
passing an unknown `comments` prop to DiffViewer (should be
commentsByLine), and ConflictResolutionPanel destructured an unused
`agent` variable.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-06 19:52:18 +01:00

110 lines
3.2 KiB
TypeScript

import type { FileDiffDetail, FileDiff, DiffHunk, DiffLine } from "./types";
/**
* Parse a unified diff string into structured FileDiffDetail objects.
*/
export function parseUnifiedDiff(raw: string): FileDiffDetail[] {
const files: FileDiffDetail[] = [];
const fileChunks = raw.split(/^diff --git /m).filter(Boolean);
for (const chunk of fileChunks) {
const lines = chunk.split("\n");
// Extract paths from first line: "a/path b/path"
const headerMatch = lines[0]?.match(/^a\/(.+?) b\/(.+)$/);
if (!headerMatch) continue;
const oldPath = headerMatch[1];
const newPath = headerMatch[2];
const hunks: DiffHunk[] = [];
let additions = 0;
let deletions = 0;
// Scan header lines (between "diff --git" and first "@@") for /dev/null markers
let hasOldDevNull = false;
let hasNewDevNull = false;
let i = 1;
while (i < lines.length && !lines[i].startsWith("@@")) {
if (lines[i].startsWith("--- /dev/null")) hasOldDevNull = true;
if (lines[i].startsWith("+++ /dev/null")) hasNewDevNull = true;
i++;
}
while (i < lines.length) {
const hunkMatch = lines[i].match(
/^@@ -(\d+)(?:,(\d+))? \+(\d+)(?:,(\d+))? @@(.*)$/
);
if (!hunkMatch) {
i++;
continue;
}
const oldStart = parseInt(hunkMatch[1], 10);
const oldCount = parseInt(hunkMatch[2] ?? "1", 10);
const newStart = parseInt(hunkMatch[3], 10);
const newCount = parseInt(hunkMatch[4] ?? "1", 10);
const header = lines[i];
const hunkLines: DiffLine[] = [];
let oldLine = oldStart;
let newLine = newStart;
i++;
while (i < lines.length && !lines[i].startsWith("@@") && !lines[i].startsWith("diff --git ")) {
const line = lines[i];
if (line.startsWith("+")) {
hunkLines.push({
type: "added",
content: line.slice(1),
oldLineNumber: null,
newLineNumber: newLine,
});
newLine++;
additions++;
} else if (line.startsWith("-")) {
hunkLines.push({
type: "removed",
content: line.slice(1),
oldLineNumber: oldLine,
newLineNumber: null,
});
oldLine++;
deletions++;
} else if (line.startsWith(" ") || line === "") {
hunkLines.push({
type: "context",
content: line.startsWith(" ") ? line.slice(1) : line,
oldLineNumber: oldLine,
newLineNumber: newLine,
});
oldLine++;
newLine++;
} else {
// Likely "\ No newline at end of file" or similar
i++;
continue;
}
i++;
}
hunks.push({ header, oldStart, oldCount, newStart, newCount, lines: hunkLines });
}
// Derive status from header markers and path comparison
let status: FileDiff['status'];
if (hasOldDevNull) {
status = "added";
} else if (hasNewDevNull) {
status = "deleted";
} else if (oldPath !== newPath) {
status = "renamed";
} else {
status = "modified";
}
files.push({ oldPath, newPath, hunks, additions, deletions, status });
}
return files;
}