From 0d05ab7b4ee173240e1039b2927fe37110989dd3 Mon Sep 17 00:00:00 2001 From: Louis Knight-Webb Date: Fri, 20 Jun 2025 16:35:17 +0100 Subject: [PATCH] Task attempt 19940407-2ae9-4850-b151-834c900e23e3 - Final changes --- backend/src/models/task_attempt.rs | 129 ++++++++++++++++++++++------- backend/src/models/test_diff.rs | 51 ++++++++++++ test_diff_logic.py | 49 +++++++++++ 3 files changed, 201 insertions(+), 28 deletions(-) create mode 100644 backend/src/models/test_diff.rs create mode 100644 test_diff_logic.py diff --git a/backend/src/models/task_attempt.rs b/backend/src/models/task_attempt.rs index 8fa35779..6cab0f3a 100644 --- a/backend/src/models/task_attempt.rs +++ b/backend/src/models/task_attempt.rs @@ -1,11 +1,10 @@ -use anyhow::anyhow; use chrono::{DateTime, Utc}; use git2::build::CheckoutBuilder; use git2::{Error as GitError, MergeOptions, Oid, RebaseOptions, Repository}; use serde::{Deserialize, Serialize}; use sqlx::{FromRow, SqlitePool, Type}; use std::path::Path; -use tracing::{debug, error, info}; +use tracing::error; use ts_rs::TS; use uuid::Uuid; @@ -690,33 +689,16 @@ impl TaskAttempt { String::new() // File was deleted }; - // Generate diff chunks using dissimilar + // Generate line-based diff chunks if old_content != new_content { - let chunks = dissimilar::diff(&old_content, &new_content); - let mut diff_chunks = Vec::new(); + let diff_chunks = Self::generate_line_based_diff(&old_content, &new_content); - for chunk in chunks { - let diff_chunk = match chunk { - dissimilar::Chunk::Equal(text) => DiffChunk { - chunk_type: DiffChunkType::Equal, - content: text.to_string(), - }, - dissimilar::Chunk::Delete(text) => DiffChunk { - chunk_type: DiffChunkType::Delete, - content: text.to_string(), - }, - dissimilar::Chunk::Insert(text) => DiffChunk { - chunk_type: DiffChunkType::Insert, - content: text.to_string(), - }, - }; - diff_chunks.push(diff_chunk); + if !diff_chunks.is_empty() { + files.push(FileDiff { + path: path_str.to_string(), + chunks: diff_chunks, + }); } - - files.push(FileDiff { - path: path_str.to_string(), - chunks: diff_chunks, - }); } } true // Continue processing @@ -729,6 +711,97 @@ impl TaskAttempt { Ok(WorktreeDiff { files }) } + /// Generate line-based diff chunks for better display + pub fn generate_line_based_diff(old_content: &str, new_content: &str) -> Vec { + let old_lines: Vec<&str> = old_content.lines().collect(); + let new_lines: Vec<&str> = new_content.lines().collect(); + let mut chunks = Vec::new(); + + // Use a simple line-by-line comparison algorithm + let mut old_idx = 0; + let mut new_idx = 0; + + while old_idx < old_lines.len() || new_idx < new_lines.len() { + if old_idx < old_lines.len() && new_idx < new_lines.len() { + let old_line = old_lines[old_idx]; + let new_line = new_lines[new_idx]; + + if old_line == new_line { + // Lines are identical + chunks.push(DiffChunk { + chunk_type: DiffChunkType::Equal, + content: format!("{}\n", old_line), + }); + old_idx += 1; + new_idx += 1; + } else { + // Lines are different - look ahead to see if this is a modification, insertion, or deletion + let mut found_match = false; + + // Check if the new line appears later in old lines (deletion) + for look_ahead in (old_idx + 1)..std::cmp::min(old_idx + 5, old_lines.len()) { + if old_lines[look_ahead] == new_line { + // Found the new line later in old, so old_line was deleted + chunks.push(DiffChunk { + chunk_type: DiffChunkType::Delete, + content: format!("{}\n", old_line), + }); + old_idx += 1; + found_match = true; + break; + } + } + + if !found_match { + // Check if the old line appears later in new lines (insertion) + for look_ahead in (new_idx + 1)..std::cmp::min(new_idx + 5, new_lines.len()) { + if new_lines[look_ahead] == old_line { + // Found the old line later in new, so new_line was inserted + chunks.push(DiffChunk { + chunk_type: DiffChunkType::Insert, + content: format!("{}\n", new_line), + }); + new_idx += 1; + found_match = true; + break; + } + } + } + + if !found_match { + // Lines are different - treat as modification (delete old, insert new) + chunks.push(DiffChunk { + chunk_type: DiffChunkType::Delete, + content: format!("{}\n", old_line), + }); + chunks.push(DiffChunk { + chunk_type: DiffChunkType::Insert, + content: format!("{}\n", new_line), + }); + old_idx += 1; + new_idx += 1; + } + } + } else if old_idx < old_lines.len() { + // Remaining old lines (deletions) + chunks.push(DiffChunk { + chunk_type: DiffChunkType::Delete, + content: format!("{}\n", old_lines[old_idx]), + }); + old_idx += 1; + } else { + // Remaining new lines (insertions) + chunks.push(DiffChunk { + chunk_type: DiffChunkType::Insert, + content: format!("{}\n", new_lines[new_idx]), + }); + new_idx += 1; + } + } + + chunks + } + /// Get the branch status for this task attempt (ahead/behind main) pub async fn get_branch_status( pool: &SqlitePool, @@ -836,7 +909,7 @@ impl TaskAttempt { let mut last_oid: Option = None; while let Some(res) = reb.next() { match res { - Ok(op) => { + Ok(_op) => { let new_oid = reb.commit(None, &sig, None)?; last_oid = Some(new_oid); } @@ -863,7 +936,7 @@ impl TaskAttempt { repo.checkout_head(Some(CheckoutBuilder::new().force()))?; // 🔟 final check - let final_oid = repo.head()?.peel_to_commit()?.id(); + let _final_oid = repo.head()?.peel_to_commit()?.id(); Ok(main_oid.to_string()) } diff --git a/backend/src/models/test_diff.rs b/backend/src/models/test_diff.rs new file mode 100644 index 00000000..3f685968 --- /dev/null +++ b/backend/src/models/test_diff.rs @@ -0,0 +1,51 @@ +#[cfg(test)] +mod tests { + use crate::models::task_attempt::{TaskAttempt, DiffChunkType}; + + #[test] + fn test_line_based_diff() { + let old_content = "line 1\nline 2\nline 3\n"; + let new_content = "line 1\nmodified line 2\nline 3\n"; + + let chunks = TaskAttempt::generate_line_based_diff(old_content, new_content); + + // Should have: equal, delete, insert, equal + assert_eq!(chunks.len(), 4); + + // First chunk should be equal + assert_eq!(chunks[0].chunk_type, DiffChunkType::Equal); + assert_eq!(chunks[0].content, "line 1\n"); + + // Second chunk should be delete + assert_eq!(chunks[1].chunk_type, DiffChunkType::Delete); + assert_eq!(chunks[1].content, "line 2\n"); + + // Third chunk should be insert + assert_eq!(chunks[2].chunk_type, DiffChunkType::Insert); + assert_eq!(chunks[2].content, "modified line 2\n"); + + // Fourth chunk should be equal + assert_eq!(chunks[3].chunk_type, DiffChunkType::Equal); + assert_eq!(chunks[3].content, "line 3\n"); + } + + #[test] + fn test_line_insertion() { + let old_content = "line 1\nline 3\n"; + let new_content = "line 1\nline 2\nline 3\n"; + + let chunks = TaskAttempt::generate_line_based_diff(old_content, new_content); + + // Should have: equal, insert, equal + assert_eq!(chunks.len(), 3); + + assert_eq!(chunks[0].chunk_type, DiffChunkType::Equal); + assert_eq!(chunks[0].content, "line 1\n"); + + assert_eq!(chunks[1].chunk_type, DiffChunkType::Insert); + assert_eq!(chunks[1].content, "line 2\n"); + + assert_eq!(chunks[2].chunk_type, DiffChunkType::Equal); + assert_eq!(chunks[2].content, "line 3\n"); + } +} diff --git a/test_diff_logic.py b/test_diff_logic.py new file mode 100644 index 00000000..3c7cc02d --- /dev/null +++ b/test_diff_logic.py @@ -0,0 +1,49 @@ +#!/usr/bin/env python3 + +def test_diff_logic(): + """Test the logic of our line-based diff algorithm""" + + # Test case 1: Line modification + old_content = "line 1\nline 2\nline 3\n" + new_content = "line 1\nmodified line 2\nline 3\n" + + old_lines = old_content.split('\n')[:-1] # Remove empty last element + new_lines = new_content.split('\n')[:-1] + + print("Test 1 - Line modification:") + print(f"Old lines: {old_lines}") + print(f"New lines: {new_lines}") + + # Expected chunks: Equal, Delete, Insert, Equal + expected_chunks = [ + ("Equal", "line 1\n"), + ("Delete", "line 2\n"), + ("Insert", "modified line 2\n"), + ("Equal", "line 3\n") + ] + + print(f"Expected chunks: {expected_chunks}") + print() + + # Test case 2: Line insertion + old_content = "line 1\nline 3\n" + new_content = "line 1\nline 2\nline 3\n" + + old_lines = old_content.split('\n')[:-1] + new_lines = new_content.split('\n')[:-1] + + print("Test 2 - Line insertion:") + print(f"Old lines: {old_lines}") + print(f"New lines: {new_lines}") + + # Expected chunks: Equal, Insert, Equal + expected_chunks = [ + ("Equal", "line 1\n"), + ("Insert", "line 2\n"), + ("Equal", "line 3\n") + ] + + print(f"Expected chunks: {expected_chunks}") + +if __name__ == "__main__": + test_diff_logic()