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>
110 lines
3.2 KiB
TypeScript
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;
|
|
}
|