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
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(
&main_repo, &old_file, &new_file, path_str,
) {
@@ -1238,6 +1240,30 @@ impl TaskAttempt {
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 diff for {}: {:?}", path_str, e);
}
@@ -1310,7 +1336,9 @@ impl TaskAttempt {
};
// 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(
&worktree_repo,
&old_file,
@@ -1323,6 +1351,30 @@ impl TaskAttempt {
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 diff for {}: {:?}", path_str, e);
}
@@ -1469,17 +1521,136 @@ impl TaskAttempt {
}
} else {
// 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) {
Ok(diff_chunks) if !diff_chunks.is_empty() => {
files.push(FileDiff {
path: path_str.to_string(),
chunks: diff_chunks,
});
// First check if this is a new file or changed file by comparing with base
let base_content = if let Ok(base_commit) = worktree_repo.find_commit(base_oid) {
if let Ok(base_tree) = base_commit.tree() {
match base_tree.get_path(std::path::Path::new(path_str)) {
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) => {
eprintln!("Error generating unstaged diff for {}: {:?}", path_str, e);
} else {
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) */}
{showDiffs && (
<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 ? (
<div className="flex items-center justify-center h-32">
@@ -1091,7 +1091,7 @@ export function TaskDetailsPanel({
{/* Bottom area - Agent Logs (responsive height) */}
<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
ref={scrollContainerRef}