Update diff viewer

This commit is contained in:
Anish Lakhwara 2025-11-08 21:15:14 -08:00
parent e04c67133b
commit 35983681e4
3 changed files with 147 additions and 91 deletions

View file

@ -1579,14 +1579,39 @@ function renderDiffFileList() {
const item = document.createElement("div");
item.className = `diff-file-item ${index === selectedFileIndex ? 'active' : ''}`;
item.innerHTML = `
<div class="diff-file-path">${file.path}</div>
<div class="diff-file-stats">
<span class="diff-file-status ${file.status}">${file.status}</span>
${file.additions > 0 ? `<span class="diff-file-additions">+${file.additions}</span>` : ''}
${file.deletions > 0 ? `<span class="diff-file-deletions">-${file.deletions}</span>` : ''}
</div>
`;
// Create file path div and set text content securely
const pathDiv = document.createElement("div");
pathDiv.className = "diff-file-path";
pathDiv.textContent = file.path;
// Create stats div
const statsDiv = document.createElement("div");
statsDiv.className = "diff-file-stats";
// Create status badge
const statusSpan = document.createElement("span");
statusSpan.className = `diff-file-status ${file.status}`;
statusSpan.textContent = file.status;
statsDiv.appendChild(statusSpan);
// Add additions count if present
if (file.additions > 0) {
const additionsSpan = document.createElement("span");
additionsSpan.className = "diff-file-additions";
additionsSpan.textContent = `+${file.additions}`;
statsDiv.appendChild(additionsSpan);
}
// Add deletions count if present
if (file.deletions > 0) {
const deletionsSpan = document.createElement("span");
deletionsSpan.className = "diff-file-deletions";
deletionsSpan.textContent = `-${file.deletions}`;
statsDiv.appendChild(deletionsSpan);
}
item.appendChild(pathDiv);
item.appendChild(statsDiv);
item.addEventListener("click", () => {
selectedFileIndex = index;
@ -1610,6 +1635,12 @@ async function renderSelectedDiff() {
const file = currentDiffFiles[selectedFileIndex];
try {
// Check if file is binary
if (file.binary) {
diffDisplay.innerHTML = '<div class="diff-empty">Binary file changed</div>';
return;
}
if (!pierreLib) {
diffDisplay.innerHTML = '<div class="diff-empty">Loading diff viewer...</div>';
return;

190
server.ts
View file

@ -234,6 +234,74 @@ async function removeGitBranch(projectDir: string, branchName: string) {
}
}
function parsePatchToFileDiffs(patch: string): FileDiff[] {
const fileDiffs: FileDiff[] = [];
// Split patch into individual file chunks
// Each chunk starts with "diff --git"
const fileChunks = patch.split(/(?=diff --git )/g).filter(chunk => chunk.trim());
for (const chunk of fileChunks) {
const lines = chunk.split('\n');
// Extract file paths from "diff --git a/... b/..."
const diffGitMatch = lines[0].match(/^diff --git a\/(.+?) b\/(.+?)$/);
if (!diffGitMatch) continue;
let oldPath = diffGitMatch[1];
let newPath = diffGitMatch[2];
// Determine status and paths
let status: FileDiff['status'] = 'modified';
let finalPath = newPath;
let finalOldPath: string | undefined;
let isBinary = false;
// Check for file mode changes and status indicators
for (const line of lines) {
if (line.startsWith('new file mode')) {
status = 'added';
finalOldPath = undefined;
} else if (line.startsWith('deleted file mode')) {
status = 'deleted';
finalPath = oldPath;
finalOldPath = undefined;
} else if (line.startsWith('rename from')) {
status = 'renamed';
finalOldPath = line.substring('rename from '.length).trim();
} else if (line.startsWith('rename to')) {
finalPath = line.substring('rename to '.length).trim();
} else if (line.startsWith('Binary files')) {
isBinary = true;
}
}
// Count additions and deletions
let additions = 0;
let deletions = 0;
for (const line of lines) {
if (line.startsWith('+') && !line.startsWith('+++')) {
additions++;
} else if (line.startsWith('-') && !line.startsWith('---')) {
deletions++;
}
}
fileDiffs.push({
path: finalPath,
oldPath: finalOldPath,
status,
diff: chunk,
additions,
deletions,
binary: isBinary,
});
}
return fileDiffs;
}
// Socket.IO handlers
io.on("connection", (socket) => {
console.log("Client connected:", socket.id);
@ -508,97 +576,53 @@ io.on("connection", (socket) => {
const git = simpleGit(session.worktreePath);
const parentBranch = session.config.parentBranch;
// Get diff between parent branch and working directory (includes all changes: staged, unstaged, committed)
const diffOutput = await git.diff([parentBranch, '--numstat']);
const fullDiffOutput = await git.diff([parentBranch]);
console.log(`[${sessionId}] Generating unified diff for session, parent branch: ${parentBranch}`);
console.log(`[${sessionId}] Worktree path: ${session.worktreePath}`);
// Also get untracked files (new files not yet added to git)
// Get unified patch for tracked changes (includes staged, unstaged, and committed)
const trackedPatch = await git.raw([
'diff',
parentBranch,
'--patch',
'--find-renames',
'--binary'
]);
// Get untracked files
const statusSummary = await git.status();
console.log(`[${sessionId}] Diff request for session, parent branch: ${parentBranch}`);
console.log(`[${sessionId}] Worktree path: ${session.worktreePath}`);
console.log(`[${sessionId}] Diff output length: ${diffOutput.length} chars`);
console.log(`[${sessionId}] Untracked files: ${statusSummary.not_added.length}`);
// Parse diff output into FileDiff array
const fileDiffs: FileDiff[] = [];
const lines = diffOutput.split('\n').filter(l => l.trim());
for (const line of lines) {
// Parse numstat line: "additions deletions filename"
const match = line.match(/^(\d+|-)\s+(\d+|-)\s+(.+)$/);
if (match) {
const [, addStr, delStr, filepath] = match;
const additions = addStr === '-' ? 0 : parseInt(addStr, 10);
const deletions = delStr === '-' ? 0 : parseInt(delStr, 10);
// Determine file status
let status: FileDiff['status'] = 'modified';
let path = filepath;
let oldPath: string | undefined;
// Check if file is renamed (contains " => ")
if (filepath.includes(' => ')) {
status = 'renamed';
const parts = filepath.split(' => ');
oldPath = parts[0].replace(/^\{|\}$/g, '');
path = parts[1].replace(/^\{|\}$/g, '');
} else if (additions > 0 && deletions === 0) {
status = 'added';
} else if (additions === 0 && deletions > 0) {
status = 'deleted';
}
// Extract individual file diff from full output
const fileDiffMatch = fullDiffOutput.match(
new RegExp(`diff --git a/${path.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')} b/${path.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}[\\s\\S]*?(?=diff --git|$)`)
);
const diff = fileDiffMatch ? fileDiffMatch[0] : '';
fileDiffs.push({
path,
oldPath,
status,
diff,
additions,
deletions,
});
}
}
// Add untracked files as "added" with full file content as diff
// Generate patches for untracked files using --no-index
const untrackedPatches: string[] = [];
for (const untrackedFile of statusSummary.not_added) {
try {
// Read the file content
const fileContent = await fs.promises.readFile(
path.join(session.worktreePath, untrackedFile),
'utf-8'
);
const lines = fileContent.split('\n');
// Create a diff in unified format for new file
const diffLines = [
`diff --git a/${untrackedFile} b/${untrackedFile}`,
`new file mode 100644`,
`--- /dev/null`,
`+++ b/${untrackedFile}`,
`@@ -0,0 +1,${lines.length} @@`,
...lines.map(line => `+${line}`)
];
fileDiffs.push({
path: untrackedFile,
status: 'added',
diff: diffLines.join('\n'),
additions: lines.length,
deletions: 0,
});
} catch (error) {
console.error(`[${sessionId}] Error reading untracked file ${untrackedFile}:`, error);
const untrackedPatch = await git.raw([
'diff',
'--no-index',
'--binary',
'/dev/null',
untrackedFile
]);
untrackedPatches.push(untrackedPatch);
} catch (error: any) {
// git diff --no-index exits with code 1 when there are differences, which is expected
if (error.message && typeof error.message === 'string') {
untrackedPatches.push(error.message);
} else {
console.error(`[${sessionId}] Error generating patch for untracked file ${untrackedFile}:`, error);
}
}
}
console.log(`[${sessionId}] Parsed ${fileDiffs.length} file diffs (${statusSummary.not_added.length} untracked)`);
// Combine all patches
const combinedPatch = [trackedPatch, ...untrackedPatches].filter(p => p).join('\n');
console.log(`[${sessionId}] Combined patch length: ${combinedPatch.length} chars`);
console.log(`[${sessionId}] Untracked files: ${statusSummary.not_added.length}`);
// Parse the combined patch into FileDiff array
const fileDiffs: FileDiff[] = parsePatchToFileDiffs(combinedPatch);
console.log(`[${sessionId}] Parsed ${fileDiffs.length} file diffs`);
socket.emit("session-diff-data", sessionId, fileDiffs);
} catch (error) {
console.error("Error getting session diff:", error);

View file

@ -26,4 +26,5 @@ export interface FileDiff {
diff: string;
additions: number;
deletions: number;
binary?: boolean;
}