//! conflict Operation trait implementation for GitRepository. use std::collections::HashMap; use gix::object::tree::diff::ChangeDetached; use crate::GitRepository; use crate::conflict::impls::ConflictOps; use crate::conflict::types::{ConflictDetail, ConflictFile, MergeStatus}; 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()) } fn changes_to_conflicts(changes: &[ChangeDetached]) -> Vec { let mut out: Vec = Vec::new(); for change in changes { match change { ChangeDetached::Addition { location, id, .. } => { out.push(ConflictFile { path: location.to_string(), our_oid: Some(*id), their_oid: None, base_oid: None, }); } ChangeDetached::Deletion { location, id, .. } => { out.push(ConflictFile { path: location.to_string(), our_oid: None, their_oid: None, base_oid: Some(*id), }); } ChangeDetached::Modification { location, previous_id, id, .. } => { out.push(ConflictFile { path: location.to_string(), our_oid: Some(*id), their_oid: None, base_oid: Some(*previous_id), }); } ChangeDetached::Rewrite { source_location, source_id, location, id, .. } => { out.push(ConflictFile { path: format!("{} -> {}", source_location, location), our_oid: Some(*id), their_oid: None, base_oid: Some(*source_id), }); } } } out } fn changes_to_oid_map(changes: &[ChangeDetached]) -> HashMap> { let mut out: HashMap> = HashMap::new(); for change in changes { match change { ChangeDetached::Addition { location, id, .. } => { out.insert(location.to_string(), Some(*id)); } ChangeDetached::Deletion { location, .. } => { out.insert(location.to_string(), None); } ChangeDetached::Modification { location, id, .. } => { out.insert(location.to_string(), Some(*id)); } ChangeDetached::Rewrite { location, id, .. } => { out.insert(location.to_string(), Some(*id)); } } } out } impl ConflictOps for GitRepository { fn conflict_check(&self, base: &str, head: &str) -> Result { let tip = self.tip_id()?; let key = format!("{base}.{head}"); if let Some(hit) = self.cache_get_rev::("conflict", "check", &key, tip) { return Ok(hit); } let repo = self.repo()?; 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 head_commit = repo .find_commit(h) .map_err(|e| crate::Error::Find(e.to_string()))?; let head_tree_id = head_commit .tree_id() .map_err(|e| crate::Error::Find(e.to_string()))? .detach(); let base_tree_id = resolve_tree_id(&repo, base)?; let fast_forwardable = repo .merge_base(b, h) .map(|mb| mb.detach() == b) .unwrap_or(false); let conflicts = if base_tree_id == head_tree_id { Vec::new() } else { let base_tree = repo .find_tree(base_tree_id) .map_err(|e| crate::Error::Find(e.to_string()))?; let head_tree = repo .find_tree(head_tree_id) .map_err(|e| crate::Error::Find(e.to_string()))?; let changes = repo .diff_tree_to_tree(Some(&base_tree), Some(&head_tree), None) .map_err(|e| crate::Error::Find(e.to_string()))?; changes_to_conflicts(&changes) }; let v = MergeStatus { mergeable: conflicts.is_empty(), conflicts, fast_forwardable, }; self.cache_put_rev("conflict", "check", &key, tip, &v); Ok(v) } fn conflict_check_threeway( &self, ours: &str, theirs: &str, ancestor: &str, ) -> Result { let repo = self.repo()?; let ours_id = repo .rev_parse_single(ours.as_bytes()) .map_err(|e| crate::Error::RevParse(e.to_string()))? .detach(); let theirs_id = repo .rev_parse_single(theirs.as_bytes()) .map_err(|e| crate::Error::RevParse(e.to_string()))? .detach(); let fast_forwardable = repo .merge_base(ours_id, theirs_id) .map(|mb| mb.detach() == ours_id) .unwrap_or(false); let ancestor_tree_id = resolve_tree_id(&repo, ancestor)?; let ours_tree_id = resolve_tree_id(&repo, ours)?; let theirs_tree_id = resolve_tree_id(&repo, theirs)?; if ours_tree_id == theirs_tree_id { return Ok(MergeStatus { mergeable: true, conflicts: Vec::new(), fast_forwardable, }); } let ancestor_tree = repo .find_tree(ancestor_tree_id) .map_err(|e| crate::Error::Find(e.to_string()))?; let ours_tree = repo .find_tree(ours_tree_id) .map_err(|e| crate::Error::Find(e.to_string()))?; let theirs_tree = repo .find_tree(theirs_tree_id) .map_err(|e| crate::Error::Find(e.to_string()))?; let ancestor_to_ours = repo .diff_tree_to_tree(Some(&ancestor_tree), Some(&ours_tree), None) .map_err(|e| crate::Error::Find(e.to_string()))?; let ancestor_to_theirs = repo .diff_tree_to_tree(Some(&ancestor_tree), Some(&theirs_tree), None) .map_err(|e| crate::Error::Find(e.to_string()))?; let ours_map = changes_to_oid_map(&ancestor_to_ours); let theirs_map = changes_to_oid_map(&ancestor_to_theirs); let mut conflicts: Vec = Vec::new(); for (path, their_oid) in &theirs_map { if let Some(our_oid) = ours_map.get(path) && our_oid != their_oid { conflicts.push(ConflictFile { path: path.clone(), our_oid: *our_oid, their_oid: *their_oid, base_oid: None, }); } } Ok(MergeStatus { mergeable: conflicts.is_empty(), conflicts, fast_forwardable, }) } fn conflict_conflicted_files( &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::>("conflict", "files", &key, tip) { return Ok(hit); } let status = self.conflict_check(base, head)?; let v: Vec = status.conflicts.into_iter().map(|c| c.path).collect(); self.cache_put_rev("conflict", "files", &key, tip, &v); Ok(v) } fn conflict_detail( &self, base: &str, head: &str, path: &str, ) -> Result { let tip = self.tip_id()?; let key = format!("{base}.{head}.{path}"); if let Some(hit) = self.cache_get_rev::("conflict", "detail", &key, tip) { return Ok(hit); } let status = self.conflict_check(base, head)?; let _conflict = status.conflicts.into_iter().find(|c| c.path == path); let v = ConflictDetail { path: path.to_string(), diff: Vec::new(), }; self.cache_put_rev("conflict", "detail", &key, tip, &v); Ok(v) } }