//! diff Operation trait implementation for GitRepository. use std::convert::Infallible; use std::ops::ControlFlow; use crate::GitRepository; use crate::diff::impls::DiffOps; use crate::diff::types::{Diff, DiffFile, DiffFileType, DiffShortStat, DiffTreeRecord}; /// Internal record for changes collected from the tree delta. #[derive(Debug, Clone)] struct ChangeRecord { status: String, head_path: String, base_path: String, head_mode: String, base_mode: String, head_oid: gix::hash::ObjectId, base_oid: gix::hash::ObjectId, } fn collect_changes( repo: &gix::Repository, base_id: gix::hash::ObjectId, head_id: gix::hash::ObjectId, ) -> Result, crate::Error> { let base_tree = repo .find_tree(base_id) .map_err(|e| crate::Error::Find(e.to_string()))?; let head_tree = repo .find_tree(head_id) .map_err(|e| crate::Error::Find(e.to_string()))?; let mut out: Vec = Vec::new(); let mut for_each = |change: gix::object::tree::diff::Change| -> Result, Infallible> { match change { gix::object::tree::diff::Change::Addition { location, entry_mode, id, .. } => { out.push(ChangeRecord { status: "added".to_string(), head_path: location.to_string(), base_path: String::new(), head_mode: format!("{:o}", entry_mode.value()), base_mode: String::new(), head_oid: id.detach(), base_oid: gix::hash::ObjectId::null(repo.object_hash()), }); } gix::object::tree::diff::Change::Deletion { location, entry_mode, id, .. } => { out.push(ChangeRecord { status: "deleted".to_string(), head_path: String::new(), base_path: location.to_string(), head_mode: String::new(), base_mode: format!("{:o}", entry_mode.value()), head_oid: gix::hash::ObjectId::null(repo.object_hash()), base_oid: id.detach(), }); } gix::object::tree::diff::Change::Modification { location, previous_entry_mode, previous_id, entry_mode, id, .. } => { out.push(ChangeRecord { status: "modified".to_string(), head_path: location.to_string(), base_path: location.to_string(), head_mode: format!("{:o}", entry_mode.value()), base_mode: format!("{:o}", previous_entry_mode.value()), head_oid: id.detach(), base_oid: previous_id.detach(), }); } gix::object::tree::diff::Change::Rewrite { source_location, source_entry_mode, source_id, location, entry_mode, id, copy, .. } => { let status = if copy { "copied" } else { "renamed" }; out.push(ChangeRecord { status: status.to_string(), head_path: location.to_string(), base_path: source_location.to_string(), head_mode: format!("{:o}", entry_mode.value()), base_mode: format!("{:o}", source_entry_mode.value()), head_oid: id.detach(), base_oid: source_id.detach(), }); } } Ok(ControlFlow::Continue(())) }; base_tree .changes() .map_err(|e| crate::Error::Find(e.to_string()))? .for_each_to_obtain_tree(&head_tree, &mut for_each) .map_err(|e| crate::Error::Find(e.to_string()))?; drop(base_tree); Ok(out) } fn resolve_tree_id(repo: &gix::Repository, rev: &str) -> Result { let id = repo .rev_parse_single(rev.as_bytes()) .map_err(|e| crate::Error::RevParse(e.to_string()))? .detach(); let commit = repo .find_commit(id) .map_err(|e| crate::Error::Find(e.to_string()))?; Ok(commit .tree_id() .map_err(|e| crate::Error::Find(e.to_string()))? .detach()) } impl DiffOps for GitRepository { fn diff_diff(&self, base: &str, head: &str) -> Result { let tip = self.tip_id()?; let key = format!("{base}.{head}"); if let Some(hit) = self.cache_get_rev::("diff", "diff", &key, tip) { return Ok(hit); } let repo = self.repo()?; let base_tree_id = resolve_tree_id(&repo, base)?; let head_tree_id = resolve_tree_id(&repo, head)?; let records = collect_changes(&repo, base_tree_id, head_tree_id)?; let files = records .into_iter() .map(|r| { let (tp, name, old_name) = match r.status.as_str() { "added" => (DiffFileType::Add, r.head_path.clone(), String::new()), "deleted" => (DiffFileType::Del, r.head_path.clone(), r.base_path.clone()), "renamed" => ( DiffFileType::Rename, r.head_path.clone(), r.base_path.clone(), ), "copied" => (DiffFileType::Copy, r.head_path.clone(), r.base_path.clone()), _ => ( DiffFileType::Change, r.head_path.clone(), r.base_path.clone(), ), }; DiffFile { name, old_name, addition: 0, deletion: 0, r#type: tp, entry_mode: r.head_mode, old_entry_mode: r.base_mode, is_created: r.status == "added", is_deleted: r.status == "deleted", is_bin: false, is_lfs_file: false, is_renamed: r.status == "renamed", is_submodule: false, is_generated: false, is_vendored: false, is_incomplete: false, is_incomplete_line_too_long: false, sections: Vec::new(), left_blob_size: 0, right_blob_size: 0, left_blob_mime_type: String::new(), right_blob_mime_type: String::new(), is_blob_type_image: false, is_blob_type_csv: false, } }) .collect(); let v = Diff { start: base.to_string(), end: head.to_string(), files, is_incomplete: false, }; self.cache_put_rev("diff", "diff", &key, tip, &v); Ok(v) } fn diff_short_stat(&self, base: &str, head: &str) -> Result { let tip = self.tip_id()?; let key = format!("{base}.{head}"); if let Some(hit) = self.cache_get_rev::("diff", "short", &key, tip) { return Ok(hit); } let repo = self.repo()?; let base_tree_id = resolve_tree_id(&repo, base)?; let head_tree_id = resolve_tree_id(&repo, head)?; let records = collect_changes(&repo, base_tree_id, head_tree_id)?; let v = DiffShortStat { num_files: records.len() as i32, total_addition: 0, total_deletion: 0, }; self.cache_put_rev("diff", "short", &key, tip, &v); Ok(v) } fn diff_files_changed_between( &self, base: &str, head: &str, ) -> Result, crate::Error> { let tip = self.tip_id()?; let key = format!("{base}.{head}"); if let Some(hit) = self.cache_get_rev::>("diff", "files", &key, tip) { return Ok(hit); } let repo = self.repo()?; let base_tree_id = resolve_tree_id(&repo, base)?; let head_tree_id = resolve_tree_id(&repo, head)?; let records = collect_changes(&repo, base_tree_id, head_tree_id)?; let v: Vec = records.into_iter().map(|r| r.head_path).collect(); self.cache_put_rev("diff", "files", &key, tip, &v); Ok(v) } fn diff_tree( &self, base: &str, head: &str, use_merge_base: bool, ) -> Result, crate::Error> { let tip = self.tip_id()?; let key = format!("{base}.{head}.{use_merge_base}"); if let Some(hit) = self.cache_get_rev::>("diff", "tree", &key, tip) { return Ok(hit); } let repo = self.repo()?; let (base_rev, head_rev) = if use_merge_base { let h = repo .rev_parse_single(head.as_bytes()) .map_err(|e| crate::Error::RevParse(e.to_string()))? .detach(); let b = repo .rev_parse_single(base.as_bytes()) .map_err(|e| crate::Error::RevParse(e.to_string()))? .detach(); let merged = repo .merge_base(b, h) .map_err(|e| crate::Error::Find(e.to_string()))?; (merged.to_string(), h.to_string()) } else { (base.to_string(), head.to_string()) }; let base_tree_id = resolve_tree_id(&repo, &base_rev)?; let head_tree_id = resolve_tree_id(&repo, &head_rev)?; let records = collect_changes(&repo, base_tree_id, head_tree_id)?; let v: Vec = records .into_iter() .map(|r| DiffTreeRecord { status: r.status, score: 0, head_path: r.head_path, base_path: r.base_path, head_mode: r.head_mode, base_mode: r.base_mode, head_blob_id: r.head_oid, base_blob_id: r.base_oid, }) .collect(); self.cache_put_rev("diff", "tree", &key, tip, &v); Ok(v) } }