diff --git a/packages/tauri/src/deltas/document.rs b/packages/tauri/src/deltas/document.rs index d01f7dc2a..80c0fd3cc 100644 --- a/packages/tauri/src/deltas/document.rs +++ b/packages/tauri/src/deltas/document.rs @@ -51,8 +51,10 @@ impl Document { let operations = operations::get_delta_operations(&self.to_string(), new_text); let delta = if operations.is_empty() { - if matches!(value, Some(reader::Content::UTF8(_))) { - return Ok(None); + if let Some(reader::Content::UTF8(value)) = value { + if !value.is_empty() { + return Ok(None); + } } delta::Delta { diff --git a/packages/tauri/src/git/diff.rs b/packages/tauri/src/git/diff.rs index 7dd1d8161..1e768c122 100644 --- a/packages/tauri/src/git/diff.rs +++ b/packages/tauri/src/git/diff.rs @@ -9,10 +9,10 @@ use super::Repository; #[derive(Debug, PartialEq, Clone, Serialize)] pub struct Hunk { - pub old_start: usize, - pub old_lines: usize, - pub new_start: usize, - pub new_lines: usize, + pub old_start: u32, + pub old_lines: u32, + pub new_start: u32, + pub new_lines: u32, pub diff: String, pub binary: bool, } @@ -72,19 +72,8 @@ fn hunks_by_filepath( repository: &Repository, diff: &git2::Diff, ) -> Result>> { - use std::fmt::Write as _; - // find all the hunks let mut hunks_by_filepath: HashMap> = HashMap::new(); - let mut current_diff = String::new(); - - let mut current_file_path: Option = None; - let mut current_hunk_id: Option = None; - let mut current_new_start: Option = None; - let mut current_new_lines: Option = None; - let mut current_old_start: Option = None; - let mut current_old_lines: Option = None; - let mut current_binary = false; diff.print(git2::DiffFormat::Patch, |delta, hunk, line| { let file_path = delta.new_file().path().unwrap_or_else(|| { @@ -94,60 +83,26 @@ fn hunks_by_filepath( .expect("failed to get file name from diff") }); - if current_file_path.is_none() { - current_file_path = Some(file_path.to_path_buf()); - } + hunks_by_filepath + .entry(file_path.to_path_buf()) + .or_default(); - let (hunk_id, new_start, new_lines, old_start, old_lines) = if let Some(hunk) = hunk { - ( + let new_start = hunk.as_ref().map_or(0, git2::DiffHunk::new_start); + let new_lines = hunk.as_ref().map_or(0, git2::DiffHunk::new_lines); + let old_start = hunk.as_ref().map_or(0, git2::DiffHunk::old_start); + let old_lines = hunk.as_ref().map_or(0, git2::DiffHunk::old_lines); + + if let Some((line, is_binary)) = match line.origin() { + '+' | '-' | ' ' => Some(( format!( - "{}-{} {}-{}", - hunk.new_start(), - hunk.new_lines(), - hunk.old_start(), - hunk.old_lines(), - ), - hunk.new_start(), - hunk.new_lines(), - hunk.old_start(), - hunk.old_lines(), - ) - } else if line.origin() == 'B' { - let hunk_id = format!("{:?}:{}", file_path.as_os_str(), delta.new_file().id()); - (hunk_id.clone(), 0, 0, 0, 0) - } else { - return true; - }; - - let is_path_changed = current_file_path - .as_ref() - .map_or(false, |p| !file_path.eq(p)); - - let is_hunk_changed = current_hunk_id.as_ref().map_or(false, |h| !hunk_id.eq(h)); - - if is_hunk_changed || is_path_changed { - let file_path = current_file_path.as_ref().unwrap().clone(); - hunks_by_filepath.entry(file_path).or_default().push(Hunk { - old_start: current_old_start.unwrap(), - old_lines: current_old_lines.unwrap(), - new_start: current_new_start.unwrap(), - new_lines: current_new_lines.unwrap(), - diff: current_diff.clone(), - binary: current_binary, - }); - current_diff = String::new(); - } - - match line.origin() { - '+' | '-' | ' ' => { - let _ = write!(current_diff, "{}", line.origin()); - current_diff.push_str( + "{}{}", + line.origin(), str::from_utf8(line.content()) .map_err(|error| tracing::error!(?error, ?file_path)) - .unwrap_or_default(), - ); - current_binary = false; - } + .unwrap_or_default() + ), + false, + )), 'B' => { let full_path = repository.workdir().unwrap().join(file_path); // save the file_path to the odb @@ -155,40 +110,188 @@ fn hunks_by_filepath( // the binary file wasnt deleted repository.blob_path(full_path.as_path()).unwrap(); } - let _ = write!(current_diff, "{}", delta.new_file().id()); - current_binary = true; + Some((delta.new_file().id().to_string(), true)) } - _ => { - current_diff.push_str( - str::from_utf8(line.content()) - .map_err(|error| tracing::error!(?error, ?file_path)) - .unwrap_or_default(), - ); + 'F' => None, + _ => Some(( + str::from_utf8(line.content()) + .map_err(|error| tracing::error!(?error, ?file_path)) + .unwrap_or_default() + .to_string(), + false, + )), + } { + let hunks = hunks_by_filepath + .entry(file_path.to_path_buf()) + .or_default(); + + if let Some(hunk) = hunks.last_mut() { + if hunk.old_start == old_start + && hunk.old_lines == old_lines + && hunk.new_start == new_start + && hunk.new_lines == new_lines + { + hunk.diff.push_str(&line); + hunk.binary |= is_binary; + } else { + hunks.push(Hunk { + old_start, + old_lines, + new_start, + new_lines, + diff: line, + binary: is_binary, + }); + } + } else { + hunks.push(Hunk { + old_start, + old_lines, + new_start, + new_lines, + diff: line, + binary: is_binary, + }); } } - current_file_path = Some(file_path.to_path_buf()); - current_hunk_id = Some(hunk_id); - current_new_start = Some(new_start as usize); - current_new_lines = Some(new_lines as usize); - current_old_start = Some(old_start as usize); - current_old_lines = Some(old_lines as usize); - true }) .context("failed to print diff")?; - // push the last hunk - if let Some(file_path) = current_file_path { - hunks_by_filepath.entry(file_path).or_default().push(Hunk { - old_start: current_old_start.unwrap_or_default(), - old_lines: current_old_lines.unwrap_or_default(), - new_start: current_new_start.unwrap_or_default(), - new_lines: current_new_lines.unwrap_or_default(), - diff: current_diff, - binary: current_binary, - }); + Ok(hunks_by_filepath + .into_iter() + .map(|(k, v)| { + if v.is_empty() { + ( + k, + vec![Hunk { + old_start: 0, + old_lines: 0, + new_start: 0, + new_lines: 0, + diff: String::new(), + binary: false, + }], + ) + } else { + (k, v) + } + }) + .collect()) +} + +#[cfg(test)] +mod tests { + use crate::test_utils; + + use super::*; + + #[test] + fn diff_simple_text() { + let repository = test_utils::test_repository(); + std::fs::write(repository.workdir().unwrap().join("file"), "hello").unwrap(); + + let head_commit_id = repository.head().unwrap().peel_to_commit().unwrap().id(); + + let diff = workdir(&repository, &head_commit_id, &Options::default()).unwrap(); + assert_eq!(diff.len(), 1); + assert_eq!( + diff[&path::PathBuf::from("file")], + vec![Hunk { + old_start: 0, + old_lines: 0, + new_start: 1, + new_lines: 1, + diff: "@@ -0,0 +1 @@\n+hello\n\\ No newline at end of file\n".to_string(), + binary: false, + }] + ); } - Ok(hunks_by_filepath) + #[test] + fn diff_empty_file() { + let repository = test_utils::test_repository(); + std::fs::write(repository.workdir().unwrap().join("first"), "").unwrap(); + + let head_commit_id = repository.head().unwrap().peel_to_commit().unwrap().id(); + + let diff = workdir(&repository, &head_commit_id, &Options::default()).unwrap(); + assert_eq!(diff.len(), 1); + assert_eq!( + diff[&path::PathBuf::from("first")], + vec![Hunk { + old_start: 0, + old_lines: 0, + new_start: 0, + new_lines: 0, + diff: String::new(), + binary: false, + }] + ); + } + + #[test] + fn diff_multiple_empty_files() { + let repository = test_utils::test_repository(); + std::fs::write(repository.workdir().unwrap().join("first"), "").unwrap(); + std::fs::write(repository.workdir().unwrap().join("second"), "").unwrap(); + + let head_commit_id = repository.head().unwrap().peel_to_commit().unwrap().id(); + + let diff = workdir(&repository, &head_commit_id, &Options::default()).unwrap(); + assert_eq!(diff.len(), 2); + assert_eq!( + diff[&path::PathBuf::from("first")], + vec![Hunk { + old_start: 0, + old_lines: 0, + new_start: 0, + new_lines: 0, + diff: String::new(), + binary: false, + }] + ); + assert_eq!( + diff[&path::PathBuf::from("second")], + vec![Hunk { + old_start: 0, + old_lines: 0, + new_start: 0, + new_lines: 0, + diff: String::new(), + binary: false, + }] + ); + } + + #[test] + fn diff_binary() { + let repository = test_utils::test_repository(); + std::fs::write( + repository.workdir().unwrap().join("image"), + [ + 255, 0, 0, // Red pixel + 0, 0, 255, // Blue pixel + 255, 255, 0, // Yellow pixel + 0, 255, 0, // Green pixel + ], + ) + .unwrap(); + + let head_commit_id = repository.head().unwrap().peel_to_commit().unwrap().id(); + + let diff = workdir(&repository, &head_commit_id, &Options::default()).unwrap(); + assert_eq!( + diff[&path::PathBuf::from("image")], + vec![Hunk { + old_start: 0, + old_lines: 0, + new_start: 0, + new_lines: 0, + diff: "71ae6e216f38164b6633e25d35abb043c3785af6".to_string(), + binary: true, + }] + ); + } } diff --git a/packages/tauri/src/virtual_branches/branch/hunk.rs b/packages/tauri/src/virtual_branches/branch/hunk.rs index 8a634adae..314d63a24 100644 --- a/packages/tauri/src/virtual_branches/branch/hunk.rs +++ b/packages/tauri/src/virtual_branches/branch/hunk.rs @@ -6,8 +6,8 @@ use anyhow::{anyhow, Context, Result}; pub struct Hunk { pub hash: Option, pub timestamp_ms: Option, - pub start: usize, - pub end: usize, + pub start: u32, + pub end: u32, } impl PartialEq for Hunk { @@ -20,8 +20,8 @@ impl PartialEq for Hunk { } } -impl From> for Hunk { - fn from(range: RangeInclusive) -> Self { +impl From> for Hunk { + fn from(range: RangeInclusive) -> Self { Hunk { start: *range.start(), end: *range.end(), @@ -38,7 +38,7 @@ impl FromStr for Hunk { let mut range = s.split('-'); let start = if let Some(raw_start) = range.next() { raw_start - .parse::() + .parse::() .context(format!("failed to parse start of range: {}", s)) } else { Err(anyhow!("invalid range: {}", s)) @@ -46,7 +46,7 @@ impl FromStr for Hunk { let end = if let Some(raw_end) = range.next() { raw_end - .parse::() + .parse::() .context(format!("failed to parse end of range: {}", s)) } else { Err(anyhow!("invalid range: {}", s)) @@ -90,8 +90,8 @@ impl Display for Hunk { impl Hunk { pub fn new( - start: usize, - end: usize, + start: u32, + end: u32, hash: Option, timestamp_ms: Option, ) -> Result { @@ -120,7 +120,7 @@ impl Hunk { self.timestamp_ms } - pub fn contains(&self, line: &usize) -> bool { + pub fn contains(&self, line: &u32) -> bool { self.start <= *line && self.end >= *line } diff --git a/packages/tauri/src/virtual_branches/virtual.rs b/packages/tauri/src/virtual_branches/virtual.rs index 8ef656dca..0258d1670 100644 --- a/packages/tauri/src/virtual_branches/virtual.rs +++ b/packages/tauri/src/virtual_branches/virtual.rs @@ -105,8 +105,8 @@ pub struct VirtualBranchHunk { pub modified_at: u128, pub file_path: path::PathBuf, pub hash: String, - pub start: usize, - pub end: usize, + pub start: u32, + pub end: u32, pub binary: bool, pub locked: bool, } diff --git a/packages/tauri/src/watcher/handlers/project_file_change.rs b/packages/tauri/src/watcher/handlers/project_file_change.rs index d02bd5fe4..d90a5727a 100644 --- a/packages/tauri/src/watcher/handlers/project_file_change.rs +++ b/packages/tauri/src/watcher/handlers/project_file_change.rs @@ -365,6 +365,34 @@ mod test { Ok(()) } + #[test] + fn test_register_empty_new_file() -> Result<()> { + let suite = Suite::default(); + let Case { + gb_repository, + project, + .. + } = suite.new_case(); + let listener = Handler::from(&suite.local_app_data); + + std::fs::write(project.path.join("test.txt"), "")?; + + listener.handle("test.txt", &project.id)?; + + let session = gb_repository.get_current_session()?.unwrap(); + let session_reader = sessions::Reader::open(&gb_repository, &session)?; + let deltas_reader = deltas::Reader::new(&session_reader); + let deltas = deltas_reader.read_file("test.txt")?.unwrap(); + assert_eq!(deltas.len(), 1); + assert_eq!(deltas[0].operations.len(), 0); + assert_eq!( + std::fs::read_to_string(gb_repository.session_wd_path().join("test.txt"))?, + "" + ); + + Ok(()) + } + #[test] fn test_register_new_file() -> Result<()> { let suite = Suite::default();