use std::collections::{BTreeSet, HashMap, HashSet, VecDeque};
use crate::error::{Error, Result};
use crate::objects::{parse_commit, ObjectId, ObjectKind};
use crate::repo::Repository;
use crate::rev_parse::resolve_revision;
pub fn resolve_commit_specs(repo: &Repository, specs: &[String]) -> Result<Vec<ObjectId>> {
let mut out = Vec::with_capacity(specs.len());
for spec in specs {
let oid = resolve_revision(repo, spec)?;
ensure_is_commit(repo, oid)?;
out.push(oid);
}
Ok(out)
}
pub fn merge_bases_first_vs_rest(
repo: &Repository,
first: ObjectId,
others: &[ObjectId],
) -> Result<Vec<ObjectId>> {
let mut cache = CommitGraphCache::new(repo);
let first_anc = cache.ancestor_closure(first)?;
let mut others_union = HashSet::new();
for &other in others {
others_union.extend(cache.ancestor_closure(other)?);
}
let candidates: HashSet<ObjectId> = first_anc.intersection(&others_union).copied().collect();
reduce_to_best(candidates, &mut cache)
}
pub fn merge_bases_octopus(repo: &Repository, commits: &[ObjectId]) -> Result<Vec<ObjectId>> {
let mut cache = CommitGraphCache::new(repo);
let mut iter = commits.iter();
let Some(&first) = iter.next() else {
return Ok(Vec::new());
};
let mut common = cache.ancestor_closure(first)?;
for &oid in iter {
let set = cache.ancestor_closure(oid)?;
common.retain(|item| set.contains(item));
}
reduce_to_best(common, &mut cache)
}
pub fn is_ancestor(repo: &Repository, ancestor: ObjectId, descendant: ObjectId) -> Result<bool> {
let mut cache = CommitGraphCache::new(repo);
if ancestor == descendant {
return Ok(true);
}
Ok(cache.ancestor_closure(descendant)?.contains(&ancestor))
}
pub fn ancestor_closure(repo: &Repository, tip: ObjectId) -> Result<HashSet<ObjectId>> {
let mut cache = CommitGraphCache::new(repo);
cache.ancestor_closure(tip)
}
pub fn count_symmetric_ahead_behind(
repo: &Repository,
local: ObjectId,
other: ObjectId,
) -> Result<(usize, usize)> {
let left = ancestor_closure(repo, local)?;
let right = ancestor_closure(repo, other)?;
let ahead = left.difference(&right).count();
let behind = right.difference(&left).count();
Ok((ahead, behind))
}
pub fn independent_commits(repo: &Repository, commits: &[ObjectId]) -> Result<Vec<ObjectId>> {
let mut cache = CommitGraphCache::new(repo);
let mut out = Vec::new();
for (i, &candidate) in commits.iter().enumerate() {
let mut reachable = false;
for (j, &other) in commits.iter().enumerate() {
if i == j {
continue;
}
if cache.ancestor_closure(other)?.contains(&candidate) {
reachable = true;
break;
}
}
if !reachable {
out.push(candidate);
}
}
Ok(out)
}
fn ensure_is_commit(repo: &Repository, oid: ObjectId) -> Result<()> {
let object = repo.odb.read(&oid)?;
if object.kind != ObjectKind::Commit {
return Err(Error::CorruptObject(format!(
"object {oid} is not a commit"
)));
}
Ok(())
}
fn reduce_to_best(
candidates: HashSet<ObjectId>,
cache: &mut CommitGraphCache<'_>,
) -> Result<Vec<ObjectId>> {
if candidates.is_empty() {
return Ok(Vec::new());
}
let mut best = BTreeSet::new();
for &candidate in &candidates {
let mut better_found = false;
for &other in &candidates {
if candidate == other {
continue;
}
if cache.ancestor_closure(other)?.contains(&candidate) {
better_found = true;
break;
}
}
if !better_found {
best.insert(candidate);
}
}
Ok(best.into_iter().collect())
}
struct CommitGraphCache<'r> {
repo: &'r Repository,
parents: HashMap<ObjectId, Vec<ObjectId>>,
closures: HashMap<ObjectId, HashSet<ObjectId>>,
}
impl<'r> CommitGraphCache<'r> {
fn new(repo: &'r Repository) -> Self {
Self {
repo,
parents: HashMap::new(),
closures: HashMap::new(),
}
}
fn ancestor_closure(&mut self, start: ObjectId) -> Result<HashSet<ObjectId>> {
if let Some(existing) = self.closures.get(&start) {
return Ok(existing.clone());
}
let mut visited = HashSet::new();
let mut queue = VecDeque::new();
queue.push_back(start);
while let Some(oid) = queue.pop_front() {
if !visited.insert(oid) {
continue;
}
for parent in self.parents_of(oid)? {
queue.push_back(parent);
}
}
self.closures.insert(start, visited.clone());
Ok(visited)
}
fn parents_of(&mut self, oid: ObjectId) -> Result<Vec<ObjectId>> {
if let Some(parents) = self.parents.get(&oid) {
return Ok(parents.clone());
}
let object = self.repo.odb.read(&oid)?;
if object.kind != ObjectKind::Commit {
return Err(Error::CorruptObject(format!(
"object {oid} is not a commit"
)));
}
let commit = parse_commit(&object.data)?;
self.parents.insert(oid, commit.parents.clone());
Ok(commit.parents)
}
}