use gix::{ bstr::ByteSlice, diff::blob::unified_diff::{ ConsumeHunk, ContextSize, DiffLineKind, HunkHeader, }, }; use crate::{ bare::GitBare, cmd::{ diff::{ DiffDelta, DiffDeltaStatus, DiffFile, DiffHunk, DiffLine, DiffOptions, DiffResult, DiffStats, }, oid::ObjectId, }, errors::{GitError, GitResult}, }; pub fn peel_to_tree( repo: &gix::Repository, oid: ObjectId, ) -> GitResult> { let gix_id: gix::hash::ObjectId = (&oid).try_into()?; if let Ok(tree) = repo.find_tree(gix_id) { Ok(tree) } else { let commit = repo .find_commit(gix_id) .map_err(|e| GitError::Gix(e.to_string()))?; let tree_id = commit .tree_id() .map_err(|e| GitError::Gix(e.to_string()))? .detach(); repo.find_tree(tree_id) .map_err(|e| GitError::Gix(e.to_string())) } } pub fn matches_pathspec(pathspec: &[String], path: &str) -> bool { if pathspec.is_empty() { return true; } pathspec .iter() .any(|spec| path == spec || path.starts_with(spec)) } pub fn change_to_delta( change: &gix::diff::tree_with_rewrites::Change, ) -> DiffDelta { use gix::diff::tree_with_rewrites::Change; match change { Change::Addition { location, id, .. } => { let path = location.to_str().unwrap_or("").to_string(); DiffDelta { status: DiffDeltaStatus::Added, old_file: DiffFile { oid: None, path: Some(path.clone()), size: 0, is_binary: false, }, new_file: DiffFile { oid: oid_to_option(id), path: Some(path), size: 0, is_binary: false, }, nfiles: 1, hunks: Vec::new(), lines: Vec::new(), } } Change::Deletion { location, id, .. } => { let path = location.to_str().unwrap_or("").to_string(); DiffDelta { status: DiffDeltaStatus::Deleted, old_file: DiffFile { oid: oid_to_option(id), path: Some(path.clone()), size: 0, is_binary: false, }, new_file: DiffFile { oid: None, path: Some(path), size: 0, is_binary: false, }, nfiles: 1, hunks: Vec::new(), lines: Vec::new(), } } Change::Modification { location, previous_entry_mode, previous_id, entry_mode, id, } => { let path = location.to_str().unwrap_or("").to_string(); let status = if previous_entry_mode.kind() != entry_mode.kind() { DiffDeltaStatus::Typechange } else { DiffDeltaStatus::Modified }; DiffDelta { status, old_file: DiffFile { oid: oid_to_option(previous_id), path: Some(path.clone()), size: 0, is_binary: false, }, new_file: DiffFile { oid: oid_to_option(id), path: Some(path), size: 0, is_binary: false, }, nfiles: 1, hunks: Vec::new(), lines: Vec::new(), } } Change::Rewrite { source_location, source_id, location, id, copy, .. } => { let old_path = source_location.to_str().unwrap_or("").to_string(); let new_path = location.to_str().unwrap_or("").to_string(); DiffDelta { status: if *copy { DiffDeltaStatus::Copied } else { DiffDeltaStatus::Renamed }, old_file: DiffFile { oid: oid_to_option(source_id), path: Some(old_path), size: 0, is_binary: false, }, new_file: DiffFile { oid: oid_to_option(id), path: Some(new_path), size: 0, is_binary: false, }, nfiles: 2, hunks: Vec::new(), lines: Vec::new(), } } } } pub fn oid_to_option(id: &gix::hash::oid) -> Option { if id.is_null() { None } else { Some(ObjectId::new(id.to_hex().to_string())) } } pub struct HunkCollector { hunks: Vec, lines: Vec, current_old_lineno: u32, current_new_lineno: u32, } impl HunkCollector { pub fn new() -> Self { HunkCollector { hunks: Vec::new(), lines: Vec::new(), current_old_lineno: 0, current_new_lineno: 0, } } } impl ConsumeHunk for HunkCollector { type Out = (Vec, Vec); fn consume_hunk( &mut self, header: HunkHeader, entries: &[(DiffLineKind, &[u8])], ) -> std::io::Result<()> { self.current_old_lineno = header.before_hunk_start; self.current_new_lineno = header.after_hunk_start; self.hunks.push(DiffHunk { old_start: header.before_hunk_start, old_lines: header.before_hunk_len, new_start: header.after_hunk_start, new_lines: header.after_hunk_len, header: format!( "@@ -{},{} +{},{} @@", header.before_hunk_start, header.before_hunk_len, header.after_hunk_start, header.after_hunk_len ), }); for (kind, content) in entries { let origin = kind.to_prefix(); let content_str = String::from_utf8_lossy(content).to_string(); let (old_lineno, new_lineno) = match kind { DiffLineKind::Context => { let old = Some(self.current_old_lineno); let new = Some(self.current_new_lineno); self.current_old_lineno += 1; self.current_new_lineno += 1; (old, new) } DiffLineKind::Add => { let new = Some(self.current_new_lineno); self.current_new_lineno += 1; (None, new) } DiffLineKind::Remove => { let old = self.current_old_lineno; self.current_old_lineno += 1; (Some(old), None) } }; self.lines.push(DiffLine { content: content_str, origin, old_lineno, new_lineno, num_lines: 1, content_offset: -1, }); } Ok(()) } fn finish(self) -> (Vec, Vec) { (self.hunks, self.lines) } } impl GitBare { pub fn diff_tree_to_tree( &self, old_tree: ObjectId, new_tree: ObjectId, opts: Option, ) -> GitResult { let repo = self.gix_repo()?; let options = opts.unwrap_or_default(); let old_tree_obj = peel_to_tree(&repo, old_tree)?; let new_tree_obj = peel_to_tree(&repo, new_tree)?; 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); // Skip directories — only diff blobs if change.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; if options.context_lines > 0 { let ctx = ContextSize::symmetrical( options.context_lines.max(3), ); 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 }) } }