Diff fixes (#101)

* Generate diffs for uncommitted changes and blank files

* Fixes
This commit is contained in:
Louis Knight-Webb
2025-07-08 17:00:13 +01:00
committed by GitHub
parent 2829686a71
commit 5d256c243a
2 changed files with 184 additions and 13 deletions

View File

@@ -1228,7 +1228,9 @@ impl TaskAttempt {
}; };
// Generate Git-native diff chunks // Generate Git-native diff chunks
if old_content != new_content { // Always generate diff for file changes, even if both contents are empty
// This handles cases where empty files are added or files are deleted
if old_content != new_content || delta.status() != git2::Delta::Modified {
match Self::generate_git_diff_chunks( match Self::generate_git_diff_chunks(
&main_repo, &old_file, &new_file, path_str, &main_repo, &old_file, &new_file, path_str,
) { ) {
@@ -1238,6 +1240,30 @@ impl TaskAttempt {
chunks: diff_chunks, chunks: diff_chunks,
}); });
} }
// For added or deleted files without content, still show the file
Ok(_)
if delta.status() == git2::Delta::Added
|| delta.status() == git2::Delta::Deleted =>
{
files.push(FileDiff {
path: path_str.to_string(),
chunks: vec![DiffChunk {
chunk_type: if delta.status() == git2::Delta::Added {
DiffChunkType::Insert
} else {
DiffChunkType::Delete
},
content: format!(
"{} file",
if delta.status() == git2::Delta::Added {
"Added"
} else {
"Deleted"
}
),
}],
});
}
Err(e) => { Err(e) => {
eprintln!("Error generating diff for {}: {:?}", path_str, e); eprintln!("Error generating diff for {}: {:?}", path_str, e);
} }
@@ -1310,7 +1336,9 @@ impl TaskAttempt {
}; };
// Generate Git-native diff chunks // Generate Git-native diff chunks
if old_content != new_content { // Always generate diff for file changes, even if both contents are empty
// This handles cases where empty files are added or files are deleted
if old_content != new_content || delta.status() != git2::Delta::Modified {
match Self::generate_git_diff_chunks( match Self::generate_git_diff_chunks(
&worktree_repo, &worktree_repo,
&old_file, &old_file,
@@ -1323,6 +1351,30 @@ impl TaskAttempt {
chunks: diff_chunks, chunks: diff_chunks,
}); });
} }
// For added or deleted files without content, still show the file
Ok(_)
if delta.status() == git2::Delta::Added
|| delta.status() == git2::Delta::Deleted =>
{
files.push(FileDiff {
path: path_str.to_string(),
chunks: vec![DiffChunk {
chunk_type: if delta.status() == git2::Delta::Added {
DiffChunkType::Insert
} else {
DiffChunkType::Delete
},
content: format!(
"{} file",
if delta.status() == git2::Delta::Added {
"Added"
} else {
"Deleted"
}
),
}],
});
}
Err(e) => { Err(e) => {
eprintln!("Error generating diff for {}: {:?}", path_str, e); eprintln!("Error generating diff for {}: {:?}", path_str, e);
} }
@@ -1469,17 +1521,136 @@ impl TaskAttempt {
} }
} else { } else {
// File only has unstaged changes (new file or uncommitted changes only) // File only has unstaged changes (new file or uncommitted changes only)
match Self::generate_git_diff_chunks(worktree_repo, &old_file, &new_file, path_str) { // First check if this is a new file or changed file by comparing with base
Ok(diff_chunks) if !diff_chunks.is_empty() => { let base_content = if let Ok(base_commit) = worktree_repo.find_commit(base_oid) {
files.push(FileDiff { if let Ok(base_tree) = base_commit.tree() {
path: path_str.to_string(), match base_tree.get_path(std::path::Path::new(path_str)) {
chunks: diff_chunks, Ok(entry) => {
}); if let Ok(blob) = worktree_repo.find_blob(entry.id()) {
String::from_utf8_lossy(blob.content()).to_string()
} else {
String::new()
}
}
Err(_) => String::new(),
}
} else {
String::new()
} }
Err(e) => { } else {
eprintln!("Error generating unstaged diff for {}: {:?}", path_str, e); String::new()
};
// Get the working directory content
let working_content = if delta.status() != git2::Delta::Deleted {
let file_path = std::path::Path::new(worktree_path).join(path_str);
std::fs::read_to_string(&file_path).unwrap_or_default()
} else {
String::new()
};
// Create diff from base to working directory (including unstaged changes)
if base_content != working_content || delta.status() != git2::Delta::Modified {
if let Ok(patch) = git2::Patch::from_buffers(
base_content.as_bytes(),
Some(std::path::Path::new(path_str)),
working_content.as_bytes(),
Some(std::path::Path::new(path_str)),
Some(&mut git2::DiffOptions::new()),
) {
let mut chunks = Vec::new();
// Process the patch hunks
for hunk_idx in 0..patch.num_hunks() {
if let Ok((_hunk, hunk_lines)) = patch.hunk(hunk_idx) {
for line_idx in 0..hunk_lines {
if let Ok(line) = patch.line_in_hunk(hunk_idx, line_idx) {
let content =
String::from_utf8_lossy(line.content()).to_string();
let chunk_type = match line.origin() {
' ' => DiffChunkType::Equal,
'+' => DiffChunkType::Insert,
'-' => DiffChunkType::Delete,
_ => continue,
};
chunks.push(DiffChunk {
chunk_type,
content,
});
}
}
}
}
// If no hunks but file status changed, add a placeholder
if chunks.is_empty() && delta.status() != git2::Delta::Modified {
chunks.push(DiffChunk {
chunk_type: if delta.status() == git2::Delta::Added {
DiffChunkType::Insert
} else {
DiffChunkType::Delete
},
content: format!(
"{} file",
if delta.status() == git2::Delta::Added {
"Added"
} else {
"Deleted"
}
),
});
}
if !chunks.is_empty() {
files.push(FileDiff {
path: path_str.to_string(),
chunks,
});
}
} else {
// Fallback to the original method if patch creation fails
match Self::generate_git_diff_chunks(
worktree_repo,
&old_file,
&new_file,
path_str,
) {
Ok(diff_chunks) if !diff_chunks.is_empty() => {
files.push(FileDiff {
path: path_str.to_string(),
chunks: diff_chunks,
});
}
// For added or deleted files without content, still show the file
Ok(_)
if delta.status() == git2::Delta::Added
|| delta.status() == git2::Delta::Deleted =>
{
files.push(FileDiff {
path: path_str.to_string(),
chunks: vec![DiffChunk {
chunk_type: if delta.status() == git2::Delta::Added {
DiffChunkType::Insert
} else {
DiffChunkType::Delete
},
content: format!(
"{} file",
if delta.status() == git2::Delta::Added {
"Added"
} else {
"Deleted"
}
),
}],
});
}
Err(e) => {
eprintln!("Error generating unstaged diff for {}: {:?}", path_str, e);
}
_ => {}
}
} }
_ => {}
} }
} }

View File

@@ -839,7 +839,7 @@ export function TaskDetailsPanel({
{/* Top area - Code Changes (responsive height) */} {/* Top area - Code Changes (responsive height) */}
{showDiffs && ( {showDiffs && (
<div <div
className={`${areAllFilesCollapsed() ? 'h-auto' : 'max-h-[66vh]'} min-h-0 ${areAllFilesCollapsed() ? 'p-2' : 'p-4'} overflow-y-auto`} className={`${areAllFilesCollapsed() ? 'h-auto' : 'max-h-[66vh]'} min-h-0 p-4 overflow-y-auto`}
> >
{diffLoading ? ( {diffLoading ? (
<div className="flex items-center justify-center h-32"> <div className="flex items-center justify-center h-32">
@@ -1091,7 +1091,7 @@ export function TaskDetailsPanel({
{/* Bottom area - Agent Logs (responsive height) */} {/* Bottom area - Agent Logs (responsive height) */}
<div <div
className={`${!showDiffs || areAllFilesCollapsed() ? 'flex-1' : 'flex-1'} min-h-0 ${showDiffs ? 'border-t' : ''} bg-muted/30`} className={`${!showDiffs || areAllFilesCollapsed() ? 'flex-1' : 'flex-1'} min-h-60 ${showDiffs ? 'border-t' : ''} bg-muted/30`}
> >
<div <div
ref={scrollContainerRef} ref={scrollContainerRef}