Merge task: Improve changes view into main

This commit is contained in:
Louis Knight-Webb
2025-06-24 17:26:03 +01:00
2 changed files with 124 additions and 52 deletions

View File

@@ -941,10 +941,15 @@ impl TaskAttempt {
// second parent is the branch that was merged
let parents: Vec<_> = merge_commit.parents().collect();
// Create diff options with more context
let mut diff_opts = git2::DiffOptions::new();
diff_opts.context_lines(10); // Include 10 lines of context around changes
diff_opts.interhunk_lines(0); // Don't merge hunks
let diff = if parents.len() >= 2 {
let base_tree = parents[0].tree()?; // Main branch before merge
let merged_tree = parents[1].tree()?; // The branch that was merged
main_repo.diff_tree_to_tree(Some(&base_tree), Some(&merged_tree), None)?
main_repo.diff_tree_to_tree(Some(&base_tree), Some(&merged_tree), Some(&mut diff_opts))?
} else {
// Fast-forward merge or single parent - compare merge commit with its parent
let base_tree = if !parents.is_empty() {
@@ -954,7 +959,7 @@ impl TaskAttempt {
main_repo.find_tree(git2::Oid::zero())?
};
let merged_tree = merge_commit.tree()?;
main_repo.diff_tree_to_tree(Some(&base_tree), Some(&merged_tree), None)?
main_repo.diff_tree_to_tree(Some(&base_tree), Some(&merged_tree), Some(&mut diff_opts))?
};
// Process each diff delta (file change)
@@ -1028,9 +1033,13 @@ impl TaskAttempt {
let current_commit = worktree_repo.find_commit(worktree_head_oid)?;
let current_tree = current_commit.tree()?;
// Create a diff between the base tree and current tree
// Create a diff between the base tree and current tree with more context
let mut diff_opts = git2::DiffOptions::new();
diff_opts.context_lines(10); // Include 10 lines of context around changes
diff_opts.interhunk_lines(0); // Don't merge hunks
let diff =
worktree_repo.diff_tree_to_tree(Some(&base_tree), Some(&current_tree), None)?;
worktree_repo.diff_tree_to_tree(Some(&base_tree), Some(&current_tree), Some(&mut diff_opts))?;
// Process each diff delta (file change)
diff.foreach(
@@ -1114,14 +1123,18 @@ impl TaskAttempt {
None
};
// Generate patch using Git's diff algorithm
// Generate patch using Git's diff algorithm with context
let mut diff_opts = git2::DiffOptions::new();
diff_opts.context_lines(10); // Include 10 lines of context around changes
diff_opts.interhunk_lines(0); // Don't merge hunks
let patch = match (old_blob.as_ref(), new_blob.as_ref()) {
(Some(old_b), Some(new_b)) => git2::Patch::from_blobs(
old_b,
Some(Path::new(file_path)),
new_b,
Some(Path::new(file_path)),
None,
Some(&mut diff_opts),
)?,
(None, Some(new_b)) => {
// File was added - diff from empty buffer to new blob content
@@ -1130,7 +1143,7 @@ impl TaskAttempt {
Some(Path::new(file_path)),
new_b.content(), // new blob content as buffer
Some(Path::new(file_path)),
None,
Some(&mut diff_opts),
)?
}
(Some(old_b), None) => {
@@ -1140,7 +1153,7 @@ impl TaskAttempt {
Some(Path::new(file_path)),
&[],
Some(Path::new(file_path)),
None,
Some(&mut diff_opts),
)?
}
(None, None) => {

View File

@@ -18,6 +18,8 @@ import {
RefreshCw,
GitBranch,
Trash2,
Eye,
EyeOff,
} from "lucide-react";
import { makeRequest } from "@/lib/api";
import type {
@@ -53,6 +55,7 @@ export function TaskAttemptComparePage() {
const [expandedSections, setExpandedSections] = useState<Set<string>>(
new Set()
);
const [showAllUnchanged, setShowAllUnchanged] = useState(false);
const [deletingFiles, setDeletingFiles] = useState<Set<string>>(new Set());
const [fileToDelete, setFileToDelete] = useState<string | null>(null);
@@ -184,13 +187,13 @@ export function TaskAttemptComparePage() {
};
const getChunkClassName = (chunkType: DiffChunkType) => {
const baseClass = "font-mono text-sm whitespace-pre px-3 py-1";
const baseClass = "font-mono text-sm whitespace-pre py-1 flex";
switch (chunkType) {
case "Insert":
return `${baseClass} bg-green-50 text-green-800 border-l-2 border-green-400`;
return `${baseClass} bg-green-50 dark:bg-green-900/20 text-green-800 dark:text-green-200 border-l-2 border-green-400 dark:border-green-500`;
case "Delete":
return `${baseClass} bg-red-50 text-red-800 border-l-2 border-red-400`;
return `${baseClass} bg-red-50 dark:bg-red-900/20 text-red-800 dark:text-red-200 border-l-2 border-red-400 dark:border-red-500`;
case "Equal":
default:
return `${baseClass} text-muted-foreground`;
@@ -212,7 +215,8 @@ export function TaskAttemptComparePage() {
interface ProcessedLine {
content: string;
chunkType: DiffChunkType;
lineNumber: number;
oldLineNumber?: number;
newLineNumber?: number;
}
interface ProcessedSection {
@@ -226,7 +230,8 @@ export function TaskAttemptComparePage() {
const processFileChunks = (chunks: DiffChunk[], fileIndex: number) => {
const CONTEXT_LINES = 3;
const lines: ProcessedLine[] = [];
let currentLineNumber = 1;
let oldLineNumber = 1;
let newLineNumber = 1;
// Convert chunks to lines with line numbers
chunks.forEach((chunk) => {
@@ -234,11 +239,28 @@ export function TaskAttemptComparePage() {
chunkLines.forEach((line, index) => {
if (index < chunkLines.length - 1 || line !== "") {
// Skip empty last line from split
lines.push({
const processedLine: ProcessedLine = {
content: line,
chunkType: chunk.chunk_type,
lineNumber: currentLineNumber++,
});
};
// Set line numbers based on chunk type
switch (chunk.chunk_type) {
case "Equal":
processedLine.oldLineNumber = oldLineNumber++;
processedLine.newLineNumber = newLineNumber++;
break;
case "Delete":
processedLine.oldLineNumber = oldLineNumber++;
// No new line number for deletions
break;
case "Insert":
processedLine.newLineNumber = newLineNumber++;
// No old line number for insertions
break;
}
lines.push(processedLine);
}
});
});
@@ -267,9 +289,10 @@ export function TaskAttemptComparePage() {
if (
contextLength <= CONTEXT_LINES * 2 ||
(!hasPrevChange && !hasNextChange)
(!hasPrevChange && !hasNextChange) ||
showAllUnchanged
) {
// Show all context if it's short or if there are no changes around it
// Show all context if it's short, no changes around it, or global toggle is on
sections.push({
type: "context",
lines: lines.slice(i, nextChangeIndex),
@@ -292,7 +315,7 @@ export function TaskAttemptComparePage() {
if (expandEnd > expandStart) {
const expandKey = `${fileIndex}-${expandStart}-${expandEnd}`;
const isExpanded = expandedSections.has(expandKey);
const isExpanded = expandedSections.has(expandKey) || showAllUnchanged;
if (isExpanded) {
sections.push({
@@ -514,13 +537,35 @@ export function TaskAttemptComparePage() {
<Card>
<CardHeader>
<CardTitle className="text-lg">
Diff: Base Commit vs. Current Worktree
</CardTitle>
<p className="text-sm text-muted-foreground">
Shows changes made in the task attempt worktree compared to the base
commit
</p>
<div className="flex items-center justify-between">
<div>
<CardTitle className="text-lg">
Diff: Base Commit vs. Current Worktree
</CardTitle>
<p className="text-sm text-muted-foreground">
Shows changes made in the task attempt worktree compared to the base
commit
</p>
</div>
<Button
variant="outline"
size="sm"
onClick={() => setShowAllUnchanged(!showAllUnchanged)}
className="flex items-center gap-2"
>
{showAllUnchanged ? (
<>
<EyeOff className="h-4 w-4" />
Hide Unchanged
</>
) : (
<>
<Eye className="h-4 w-4" />
Show All Unchanged
</>
)}
</Button>
</div>
</CardHeader>
<CardContent>
{!diff || diff.files.length === 0 ? (
@@ -562,43 +607,45 @@ export function TaskAttemptComparePage() {
{processFileChunks(file.chunks, fileIndex).map(
(section, sectionIndex) => {
if (
section.type === "context" &&
section.lines.length === 0 &&
section.expandKey
section.type === "context" &&
section.lines.length === 0 &&
section.expandKey &&
!showAllUnchanged
) {
// Render expand button
const lineCount =
parseInt(section.expandKey.split("-")[2]) -
parseInt(section.expandKey.split("-")[1]);
return (
<div key={`expand-${section.expandKey}`}>
<Button
variant="ghost"
size="sm"
onClick={() =>
toggleExpandSection(section.expandKey!)
}
className="w-full h-8 text-xs text-blue-600 hover:text-blue-800 hover:bg-blue-50 border-t border-b border-gray-200 rounded-none"
>
<ChevronDown className="h-3 w-3 mr-1" />
Show {lineCount} more lines
</Button>
</div>
);
// Render expand button (only when global toggle is off)
const lineCount =
parseInt(section.expandKey.split("-")[2]) -
parseInt(section.expandKey.split("-")[1]);
return (
<div key={`expand-${section.expandKey}`}>
<Button
variant="ghost"
size="sm"
onClick={() =>
toggleExpandSection(section.expandKey!)
}
className="w-full h-8 text-xs text-blue-600 dark:text-blue-400 hover:text-blue-800 dark:hover:text-blue-300 hover:bg-blue-50 dark:hover:bg-blue-950/50 border-t border-b border-gray-200 dark:border-gray-700 rounded-none"
>
<ChevronDown className="h-3 w-3 mr-1" />
Show {lineCount} more lines
</Button>
</div>
);
}
// Render lines (context, change, or expanded)
return (
<div key={`section-${sectionIndex}`}>
{section.type === "expanded" &&
section.expandKey && (
section.expandKey &&
!showAllUnchanged && (
<Button
variant="ghost"
size="sm"
onClick={() =>
toggleExpandSection(section.expandKey!)
}
className="w-full h-8 text-xs text-blue-600 hover:text-blue-800 hover:bg-blue-50 border-t border-b border-gray-200 rounded-none"
className="w-full h-8 text-xs text-blue-600 dark:text-blue-400 hover:text-blue-800 dark:hover:text-blue-300 hover:bg-blue-50 dark:hover:bg-blue-950/50 border-t border-b border-gray-200 dark:border-gray-700 rounded-none"
>
<ChevronUp className="h-3 w-3 mr-1" />
Hide expanded lines
@@ -609,8 +656,20 @@ export function TaskAttemptComparePage() {
key={`${sectionIndex}-${lineIndex}`}
className={getChunkClassName(line.chunkType)}
>
{getChunkPrefix(line.chunkType)}
{line.content}
<div className="flex-shrink-0 w-16 px-2 text-xs text-gray-500 dark:text-gray-400 bg-gray-50 dark:bg-gray-800 border-r border-gray-200 dark:border-gray-700 select-none">
<span className="inline-block w-6 text-right">
{line.oldLineNumber || ""}
</span>
<span className="inline-block w-6 text-right ml-1">
{line.newLineNumber || ""}
</span>
</div>
<div className="flex-1 px-3">
<span className="inline-block w-4">
{getChunkPrefix(line.chunkType)}
</span>
<span>{line.content}</span>
</div>
</div>
))}
</div>