Skip to content
Clone
a06673b
258 lines 8.8 KB Rust
//! 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<gix::hash::ObjectId, crate::Error> {
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<ConflictFile> {
let mut out: Vec<ConflictFile> = 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<String, Option<gix::hash::ObjectId>> {
let mut out: HashMap<String, Option<gix::hash::ObjectId>> = 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<MergeStatus, crate::Error> {
let tip = self.tip_id()?;
let key = format!("{base}.{head}");
if let Some(hit) = self.cache_get_rev::<MergeStatus>("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<MergeStatus, crate::Error> {
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<ConflictFile> = 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<Vec<String>, crate::Error> {
let tip = self.tip_id()?;
let key = format!("{base}.{head}");
if let Some(hit) = self.cache_get_rev::<Vec<String>>("conflict", "files", &key, tip) {
return Ok(hit);
}
let status = self.conflict_check(base, head)?;
let v: Vec<String> = 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<ConflictDetail, crate::Error> {
let tip = self.tip_id()?;
let key = format!("{base}.{head}.{path}");
if let Some(hit) = self.cache_get_rev::<ConflictDetail>("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)
}
}