use gix::{ bstr::ByteSlice, diff::blob::unified_diff::{ ConsumeHunk, ContextSize, DiffLineKind, HunkHeader, }, }; use crate::{ bare::GitBare, cmd::{ diff::{ DiffDeltaStatus, DiffOptions, DiffResult, DiffStats, SideBySideChangeType, SideBySideDiffResult, SideBySideFile, SideBySideLine, diff_tree_to_tree::{ HunkCollector, change_to_delta, matches_pathspec, peel_to_tree, }, }, oid::ObjectId, }, errors::{GitError, GitResult}, }; impl GitBare { pub fn diff_patch( &self, old_commit: ObjectId, new_commit: ObjectId, opts: Option, ) -> GitResult { let repo = self.gix_repo()?; let options = opts.unwrap_or_default(); let context_lines = options.context_lines.max(3); let old_tree_obj = peel_to_tree(&repo, old_commit)?; let new_tree_obj = peel_to_tree(&repo, new_commit)?; let mut diff_opts = gix::diff::Options::default(); diff_opts.track_path(); let changes = repo .diff_tree_to_tree(&old_tree_obj, &new_tree_obj, Some(diff_opts)) .map_err(|e| GitError::Gix(e.to_string()))?; let mut resource_cache = repo .diff_resource_cache_for_tree_diff() .map_err(|e| GitError::Gix(e.to_string()))?; let mut deltas = Vec::new(); let mut stats = DiffStats { files_changed: 0, insertions: 0, deletions: 0, }; for change in &changes { let location = change.location().to_str().unwrap_or(""); if !matches_pathspec(&options.pathspec, location) { continue; } let mut delta = change_to_delta(change); // Only diff blobs — skip trees (directories) let entry_mode = change.entry_mode(); if entry_mode.is_tree() { resource_cache.clear_resource_cache_keep_allocation(); continue; } resource_cache .set_resource_by_change(change.to_ref(), &repo.objects) .map_err(|e| GitError::Gix(e.to_string()))?; let is_binary = { use gix::diff::blob::platform::prepare_diff::Operation; let prep = resource_cache .prepare_diff() .map_err(|e| GitError::Gix(e.to_string()))?; match prep.operation { Operation::InternalDiff { algorithm } => { let input = prep.interned_input(); let diff = gix::diff::blob::diff_with_slider_heuristics( algorithm, &input, ); stats.files_changed += 1; stats.insertions += diff.count_additions() as usize; stats.deletions += diff.count_removals() as usize; let ctx = ContextSize::symmetrical(context_lines); let collector = HunkCollector::new(); let unified = gix::diff::blob::UnifiedDiff::new( &diff, &input, collector, ctx, ); let (hunks, lines) = unified .consume() .map_err(|e| GitError::Gix(e.to_string()))?; delta.hunks = hunks; delta.lines = lines; false } Operation::SourceOrDestinationIsBinary => { stats.files_changed += 1; true } Operation::ExternalCommand { .. } => { stats.files_changed += 1; false } } }; if is_binary { delta.old_file.is_binary = true; delta.new_file.is_binary = true; } resource_cache.clear_resource_cache_keep_allocation(); deltas.push(delta); } Ok(DiffResult { stats, deltas }) } pub fn diff_patch_side_by_side( &self, old_commit: ObjectId, new_commit: ObjectId, opts: Option, ) -> GitResult { let repo = self.gix_repo()?; let options = opts.unwrap_or_default(); let context_lines = options.context_lines.max(3); let old_tree_obj = peel_to_tree(&repo, old_commit)?; let new_tree_obj = peel_to_tree(&repo, new_commit)?; let mut diff_opts = gix::diff::Options::default(); diff_opts.track_path(); let changes = repo .diff_tree_to_tree(&old_tree_obj, &new_tree_obj, Some(diff_opts)) .map_err(|e| GitError::Gix(e.to_string()))?; let mut resource_cache = repo .diff_resource_cache_for_tree_diff() .map_err(|e| GitError::Gix(e.to_string()))?; let mut files: Vec = Vec::new(); let mut total_additions = 0; let mut total_deletions = 0; for change in &changes { let location = change.location().to_str().unwrap_or(""); if !matches_pathspec(&options.pathspec, location) { continue; } let delta = change_to_delta(change); let is_rename = delta.status == DiffDeltaStatus::Renamed; let path = delta.new_file.path.clone().unwrap_or_default(); // Skip directories — only diff blobs if change.entry_mode().is_tree() { total_additions += 0; total_deletions += 0; files.push(SideBySideFile { path, additions: 0, deletions: 0, is_binary: false, is_rename, lines: Vec::new(), }); resource_cache.clear_resource_cache_keep_allocation(); continue; } resource_cache .set_resource_by_change(change.to_ref(), &repo.objects) .map_err(|e| GitError::Gix(e.to_string()))?; let (file_additions, file_deletions, is_binary, sbs_lines) = { use gix::diff::blob::platform::prepare_diff::Operation; let prep = resource_cache .prepare_diff() .map_err(|e| GitError::Gix(e.to_string()))?; match prep.operation { Operation::InternalDiff { algorithm } => { let input = prep.interned_input(); let diff = gix::diff::blob::diff_with_slider_heuristics( algorithm, &input, ); let adds = diff.count_additions() as usize; let dels = diff.count_removals() as usize; let ctx = ContextSize::symmetrical(context_lines); let collector = SideBySideCollector::new(); let unified = gix::diff::blob::UnifiedDiff::new( &diff, &input, collector, ctx, ); let lines = unified .consume() .map_err(|e| GitError::Gix(e.to_string()))?; (adds, dels, false, lines) } Operation::SourceOrDestinationIsBinary => { (0, 0, true, Vec::new()) } Operation::ExternalCommand { .. } => { (0, 0, false, Vec::new()) } } }; total_additions += file_additions; total_deletions += file_deletions; files.push(SideBySideFile { path, additions: file_additions, deletions: file_deletions, is_binary, is_rename, lines: sbs_lines, }); resource_cache.clear_resource_cache_keep_allocation(); } Ok(SideBySideDiffResult { files, total_additions, total_deletions, }) } } struct SideBySideCollector { lines: Vec, pending_removed: Vec<(u32, String)>, pending_added: Vec<(u32, String)>, current_old_lineno: u32, current_new_lineno: u32, } impl SideBySideCollector { fn new() -> Self { SideBySideCollector { lines: Vec::new(), pending_removed: Vec::new(), pending_added: Vec::new(), current_old_lineno: 0, current_new_lineno: 0, } } fn flush_pending(&mut self) { let removed_count = self.pending_removed.len(); let added_count = self.pending_added.len(); let common = removed_count.min(added_count); for i in 0..common { let (left_no, old_content) = &self.pending_removed[i]; let (right_no, new_content) = &self.pending_added[i]; let change_type = if old_content == new_content { SideBySideChangeType::Unchanged } else { SideBySideChangeType::Modified }; self.lines.push(SideBySideLine { left_line_no: Some(*left_no), right_line_no: Some(*right_no), left_content: old_content.clone(), right_content: new_content.clone(), change_type, }); } for i in common..removed_count { let (left_no, old_content) = &self.pending_removed[i]; self.lines.push(SideBySideLine { left_line_no: Some(*left_no), right_line_no: None, left_content: old_content.clone(), right_content: String::new(), change_type: SideBySideChangeType::Removed, }); } for i in common..added_count { let (right_no, new_content) = &self.pending_added[i]; self.lines.push(SideBySideLine { left_line_no: None, right_line_no: Some(*right_no), left_content: String::new(), right_content: new_content.clone(), change_type: SideBySideChangeType::Added, }); } self.pending_removed.clear(); self.pending_added.clear(); } } impl ConsumeHunk for SideBySideCollector { type Out = Vec; fn consume_hunk( &mut self, header: HunkHeader, entries: &[(DiffLineKind, &[u8])], ) -> std::io::Result<()> { self.flush_pending(); self.current_old_lineno = header.before_hunk_start; self.current_new_lineno = header.after_hunk_start; for (kind, content) in entries { let content_str = String::from_utf8_lossy(content).to_string(); match kind { DiffLineKind::Context => { self.flush_pending(); self.lines.push(SideBySideLine { left_line_no: Some(self.current_old_lineno), right_line_no: Some(self.current_new_lineno), left_content: content_str.clone(), right_content: content_str, change_type: SideBySideChangeType::Unchanged, }); self.current_old_lineno += 1; self.current_new_lineno += 1; } DiffLineKind::Add => { self.pending_added .push((self.current_new_lineno, content_str)); self.current_new_lineno += 1; } DiffLineKind::Remove => { self.pending_removed .push((self.current_old_lineno, content_str)); self.current_old_lineno += 1; } } } Ok(()) } fn finish(mut self) -> Vec { self.flush_pending(); self.lines } }