use alloc::collections::{BTreeSet, VecDeque};
use alloc::vec::Vec;
use crate::error::{Error, Result};
use crate::object::tree::FileMode;
use crate::object::{Commit, ObjectType, Tag, Tree};
use crate::odb::ObjectDatabase;
use crate::oid::ObjectId;
pub fn reachable_objects<D: ObjectDatabase>(
odb: &D,
roots: &[ObjectId],
) -> Result<BTreeSet<ObjectId>> {
let exclude = BTreeSet::new();
collect_closure(odb, roots, &exclude)
}
pub fn reachable_objects_excluding<D: ObjectDatabase>(
odb: &D,
roots: &[ObjectId],
exclude: &BTreeSet<ObjectId>,
) -> Result<BTreeSet<ObjectId>> {
collect_closure(odb, roots, exclude)
}
pub fn missing_tree_objects<D: ObjectDatabase>(
odb: &D,
tree_id: &ObjectId,
) -> Result<Vec<ObjectId>> {
let mut missing = Vec::new();
let mut seen = BTreeSet::new();
collect_missing(odb, tree_id, &mut missing, &mut seen)?;
Ok(missing)
}
fn collect_missing<D: ObjectDatabase>(
odb: &D,
tree_id: &ObjectId,
missing: &mut Vec<ObjectId>,
seen: &mut BTreeSet<ObjectId>,
) -> Result<()> {
if !seen.insert(*tree_id) {
return Ok(());
}
let (ty, payload) = match odb.read(tree_id) {
Ok(v) => v,
Err(Error::NotFound(_)) => {
missing.push(*tree_id);
return Ok(());
}
Err(e) => return Err(e),
};
if ty != ObjectType::Tree {
return Ok(());
}
let tree = Tree::parse(odb.algo(), &payload)?;
for entry in tree.entries {
match entry.mode {
FileMode::Tree => collect_missing(odb, &entry.id, missing, seen)?,
FileMode::Gitlink => {} _ => {
if seen.insert(entry.id) && !odb.contains(&entry.id) {
missing.push(entry.id);
}
}
}
}
Ok(())
}
pub fn objects_to_send<D: ObjectDatabase>(
odb: &D,
wants: &[ObjectId],
haves: &[ObjectId],
) -> Result<BTreeSet<ObjectId>> {
let exclude = collect_closure(odb, haves, &BTreeSet::new())?;
collect_closure(odb, wants, &exclude)
}
pub struct ShallowResult {
pub objects: BTreeSet<ObjectId>,
pub shallow: BTreeSet<ObjectId>,
}
pub fn shallow_objects_to_send<D: ObjectDatabase>(
odb: &D,
wants: &[ObjectId],
depth: u32,
already_shallow: &BTreeSet<ObjectId>,
) -> Result<ShallowResult> {
let depth = depth.max(1);
let mut best_depth: alloc::collections::BTreeMap<ObjectId, u32> =
alloc::collections::BTreeMap::new();
let mut shallow = BTreeSet::new();
let mut trees: Vec<ObjectId> = Vec::new();
let mut commits: BTreeSet<ObjectId> = BTreeSet::new();
let mut queue: VecDeque<(ObjectId, u32)> = wants.iter().map(|w| (*w, 1u32)).collect();
while let Some((id, d)) = queue.pop_front() {
if best_depth.get(&id).is_some_and(|&prev| prev <= d) {
continue;
}
best_depth.insert(id, d);
let (ty, payload) = match odb.read(&id) {
Ok(v) => v,
Err(Error::NotFound(_)) => continue,
Err(e) => return Err(e),
};
match ty {
ObjectType::Commit => {
commits.insert(id);
let commit = Commit::parse(odb.algo(), &payload)?;
trees.push(commit.tree);
let at_boundary = d >= depth || already_shallow.contains(&id);
if at_boundary {
if !commit.parents.is_empty() {
shallow.insert(id);
}
} else {
for parent in commit.parents {
queue.push_back((parent, d + 1));
}
}
}
ObjectType::Tag => {
let tag = Tag::parse(odb.algo(), &payload)?;
queue.push_back((tag.object, d));
commits.insert(id); }
ObjectType::Tree => trees.push(id),
ObjectType::Blob => {
commits.insert(id);
}
}
}
let mut objects = collect_closure(odb, &trees, &BTreeSet::new())?;
objects.extend(commits);
Ok(ShallowResult { objects, shallow })
}
fn collect_closure<D: ObjectDatabase>(
odb: &D,
roots: &[ObjectId],
exclude: &BTreeSet<ObjectId>,
) -> Result<BTreeSet<ObjectId>> {
let mut seen = BTreeSet::new();
let mut stack: Vec<ObjectId> = roots
.iter()
.copied()
.filter(|id| !exclude.contains(id))
.collect();
while let Some(id) = stack.pop() {
if exclude.contains(&id) || !seen.insert(id) {
continue;
}
let (ty, payload) = match odb.read(&id) {
Ok(v) => v,
Err(Error::NotFound(_)) => continue, Err(e) => return Err(e),
};
match ty {
ObjectType::Commit => {
let commit = Commit::parse(odb.algo(), &payload)?;
push_if_new(&mut stack, &seen, exclude, commit.tree);
for parent in commit.parents {
push_if_new(&mut stack, &seen, exclude, parent);
}
}
ObjectType::Tree => {
let tree = Tree::parse(odb.algo(), &payload)?;
for entry in tree.entries {
if entry.mode == FileMode::Gitlink {
continue; }
push_if_new(&mut stack, &seen, exclude, entry.id);
}
}
ObjectType::Tag => {
let tag = Tag::parse(odb.algo(), &payload)?;
push_if_new(&mut stack, &seen, exclude, tag.object);
}
ObjectType::Blob => {}
}
}
Ok(seen)
}
fn push_if_new(
stack: &mut Vec<ObjectId>,
seen: &BTreeSet<ObjectId>,
exclude: &BTreeSet<ObjectId>,
id: ObjectId,
) {
if !seen.contains(&id) && !exclude.contains(&id) {
stack.push(id);
}
}
pub fn is_ancestor<D: ObjectDatabase>(
odb: &D,
ancestor: &ObjectId,
descendant: &ObjectId,
) -> Result<bool> {
if ancestor == descendant {
return Ok(true);
}
let mut seen = BTreeSet::new();
let mut queue = VecDeque::new();
queue.push_back(*descendant);
seen.insert(*descendant);
while let Some(id) = queue.pop_front() {
let payload = match odb.read_typed(&id, ObjectType::Commit) {
Ok(p) => p,
Err(Error::NotFound(_)) | Err(Error::UnexpectedType { .. }) => continue,
Err(e) => return Err(e),
};
let commit = Commit::parse(odb.algo(), &payload)?;
for parent in commit.parents {
if &parent == ancestor {
return Ok(true);
}
if seen.insert(parent) {
queue.push_back(parent);
}
}
}
Ok(false)
}
pub fn merge_base<D: ObjectDatabase>(
odb: &D,
a: &ObjectId,
b: &ObjectId,
) -> Result<Option<ObjectId>> {
let mut best: Option<(i64, ObjectId)> = None;
for id in merge_bases(odb, a, b)? {
let time = Commit::parse(odb.algo(), &odb.read_typed(&id, ObjectType::Commit)?)?
.committer
.time;
if best.is_none_or(|(t, _)| time > t) {
best = Some((time, id));
}
}
Ok(best.map(|(_, id)| id))
}
pub fn merge_bases<D: ObjectDatabase>(
odb: &D,
a: &ObjectId,
b: &ObjectId,
) -> Result<Vec<ObjectId>> {
let ancestors_of_a = commit_ancestors(odb, a)?;
let ancestors_of_b = commit_ancestors(odb, b)?;
let common: BTreeSet<ObjectId> = ancestors_of_a
.intersection(&ancestors_of_b)
.copied()
.collect();
let mut redundant = BTreeSet::new();
for id in &common {
let payload = match odb.read_typed(id, ObjectType::Commit) {
Ok(p) => p,
Err(Error::NotFound(_)) | Err(Error::UnexpectedType { .. }) => continue,
Err(e) => return Err(e),
};
for parent in Commit::parse(odb.algo(), &payload)?.parents {
if common.contains(&parent) {
redundant.insert(parent);
}
}
}
Ok(common
.into_iter()
.filter(|id| !redundant.contains(id) && odb.contains(id))
.collect())
}
fn commit_ancestors<D: ObjectDatabase>(odb: &D, tip: &ObjectId) -> Result<BTreeSet<ObjectId>> {
let mut seen = BTreeSet::new();
let mut queue = VecDeque::new();
queue.push_back(*tip);
seen.insert(*tip);
while let Some(id) = queue.pop_front() {
let payload = match odb.read_typed(&id, ObjectType::Commit) {
Ok(p) => p,
Err(Error::NotFound(_)) | Err(Error::UnexpectedType { .. }) => continue,
Err(e) => return Err(e),
};
for parent in Commit::parse(odb.algo(), &payload)?.parents {
if seen.insert(parent) {
queue.push_back(parent);
}
}
}
Ok(seen)
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct TreeDiffEntry {
pub path: alloc::vec::Vec<u8>,
pub change: TreeChange,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum TreeChange {
Added,
Modified,
Deleted,
}
pub fn diff_trees<D: ObjectDatabase>(
odb: &D,
old: Option<&ObjectId>,
new: Option<&ObjectId>,
) -> Result<alloc::vec::Vec<TreeDiffEntry>> {
let old_map = match old {
Some(id) => flatten_tree(odb, id)?,
None => alloc::collections::BTreeMap::new(),
};
let new_map = match new {
Some(id) => flatten_tree(odb, id)?,
None => alloc::collections::BTreeMap::new(),
};
let mut out = alloc::vec::Vec::new();
for (path, (old_mode, old_id)) in &old_map {
match new_map.get(path) {
None => out.push(TreeDiffEntry {
path: path.clone(),
change: TreeChange::Deleted,
}),
Some((new_mode, new_id)) if new_mode != old_mode || new_id != old_id => {
out.push(TreeDiffEntry {
path: path.clone(),
change: TreeChange::Modified,
})
}
Some(_) => {}
}
}
for path in new_map.keys() {
if !old_map.contains_key(path) {
out.push(TreeDiffEntry {
path: path.clone(),
change: TreeChange::Added,
});
}
}
out.sort_by(|a, b| a.path.cmp(&b.path));
Ok(out)
}
pub fn flatten_tree<D: ObjectDatabase>(
odb: &D,
tree_id: &ObjectId,
) -> Result<alloc::collections::BTreeMap<alloc::vec::Vec<u8>, (FileMode, ObjectId)>> {
let mut out = alloc::collections::BTreeMap::new();
flatten_into(odb, tree_id, &[], &mut out)?;
Ok(out)
}
fn flatten_into<D: ObjectDatabase>(
odb: &D,
tree_id: &ObjectId,
prefix: &[u8],
out: &mut alloc::collections::BTreeMap<alloc::vec::Vec<u8>, (FileMode, ObjectId)>,
) -> Result<()> {
let payload = odb.read_typed(tree_id, ObjectType::Tree)?;
let tree = Tree::parse(odb.algo(), &payload)?;
for entry in tree.entries {
let mut path = prefix.to_vec();
if !path.is_empty() {
path.push(b'/');
}
path.extend_from_slice(&entry.name);
if entry.mode == FileMode::Tree {
flatten_into(odb, &entry.id, &path, out)?;
} else {
out.insert(path, (entry.mode, entry.id));
}
}
Ok(())
}
struct Queued {
time: i64,
seq: u64,
id: ObjectId,
commit: Commit,
}
impl PartialEq for Queued {
fn eq(&self, other: &Self) -> bool {
self.cmp(other) == core::cmp::Ordering::Equal
}
}
impl Eq for Queued {}
impl PartialOrd for Queued {
fn partial_cmp(&self, other: &Self) -> Option<core::cmp::Ordering> {
Some(self.cmp(other))
}
}
impl Ord for Queued {
fn cmp(&self, other: &Self) -> core::cmp::Ordering {
self.time
.cmp(&other.time)
.then_with(|| other.seq.cmp(&self.seq))
}
}
pub struct RevWalk<'a, D: ObjectDatabase> {
odb: &'a D,
pending_tips: Vec<ObjectId>,
queue: alloc::collections::BinaryHeap<Queued>,
seen: BTreeSet<ObjectId>,
seq: u64,
}
impl<'a, D: ObjectDatabase> RevWalk<'a, D> {
pub fn new(odb: &'a D, tips: &[ObjectId]) -> Self {
RevWalk {
odb,
pending_tips: tips.to_vec(),
queue: alloc::collections::BinaryHeap::new(),
seen: BTreeSet::new(),
seq: 0,
}
}
fn enqueue(&mut self, id: ObjectId) -> Result<()> {
if !self.seen.insert(id) {
return Ok(());
}
let payload = match self.odb.read_typed(&id, ObjectType::Commit) {
Ok(p) => p,
Err(Error::NotFound(_)) => return Ok(()),
Err(e) => return Err(e),
};
let commit = Commit::parse(self.odb.algo(), &payload)?;
self.seq += 1;
self.queue.push(Queued {
time: commit.committer.time,
seq: self.seq,
id,
commit,
});
Ok(())
}
fn step(&mut self) -> Result<Option<(ObjectId, Commit)>> {
for tip in core::mem::take(&mut self.pending_tips) {
self.enqueue(tip)?;
}
let Some(Queued { id, commit, .. }) = self.queue.pop() else {
return Ok(None);
};
for &parent in &commit.parents {
self.enqueue(parent)?;
}
Ok(Some((id, commit)))
}
}
impl<'a, D: ObjectDatabase> Iterator for RevWalk<'a, D> {
type Item = Result<(ObjectId, Commit)>;
fn next(&mut self) -> Option<Self::Item> {
self.step().transpose()
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::object::tree::TreeEntry;
use crate::object::{Signature, Tree};
use crate::odb::MemoryOdb;
use crate::oid::HashAlgo;
fn sig() -> Signature {
Signature {
name: b"T".to_vec(),
email: b"t@e".to_vec(),
time: 0,
tz: b"+0000".to_vec(),
}
}
fn commit(odb: &MemoryOdb, tree: ObjectId, parents: Vec<ObjectId>) -> ObjectId {
commit_msg(odb, tree, parents, b"m\n")
}
fn commit_msg(odb: &MemoryOdb, tree: ObjectId, parents: Vec<ObjectId>, msg: &[u8]) -> ObjectId {
let c = Commit {
tree,
parents,
author: sig(),
committer: sig(),
extra_headers: Vec::new(),
message: msg.to_vec(),
};
odb.write(ObjectType::Commit, &c.serialize()).unwrap()
}
#[test]
fn reachability_and_send_set() {
let odb = MemoryOdb::new(HashAlgo::Sha1);
let blob = odb.write(ObjectType::Blob, b"hello\n").unwrap();
let tree = Tree {
entries: alloc::vec![TreeEntry {
mode: FileMode::Regular,
name: b"a".to_vec(),
id: blob,
}],
};
let tree_id = odb.write(ObjectType::Tree, &tree.serialize()).unwrap();
let c1 = commit(&odb, tree_id, Vec::new());
let c2 = commit(&odb, tree_id, alloc::vec![c1]);
let all = reachable_objects(&odb, &[c2]).unwrap();
assert!(all.contains(&c1) && all.contains(&c2));
assert!(all.contains(&tree_id) && all.contains(&blob));
assert_eq!(all.len(), 4);
let send = objects_to_send(&odb, &[c2], &[c1]).unwrap();
assert!(send.contains(&c2));
assert!(!send.contains(&c1) && !send.contains(&tree_id) && !send.contains(&blob));
assert_eq!(send.len(), 1);
}
#[test]
fn missing_tree_objects_reports_absent_blobs_across_subtrees() {
let full = MemoryOdb::new(HashAlgo::Sha1);
let top = full.write(ObjectType::Blob, b"top\n").unwrap();
let inner = full.write(ObjectType::Blob, b"inner\n").unwrap();
let subtree = Tree {
entries: alloc::vec![TreeEntry {
mode: FileMode::Regular,
name: b"inner.txt".to_vec(),
id: inner,
}],
};
let subtree_id = full.write(ObjectType::Tree, &subtree.serialize()).unwrap();
let root = Tree {
entries: alloc::vec![
TreeEntry {
mode: FileMode::Regular,
name: b"top.txt".to_vec(),
id: top,
},
TreeEntry {
mode: FileMode::Tree,
name: b"sub".to_vec(),
id: subtree_id,
},
],
};
let root_id = full.write(ObjectType::Tree, &root.serialize()).unwrap();
let local = MemoryOdb::new(HashAlgo::Sha1);
local.write(ObjectType::Tree, &subtree.serialize()).unwrap();
local.write(ObjectType::Tree, &root.serialize()).unwrap();
let mut missing = missing_tree_objects(&local, &root_id).unwrap();
missing.sort();
let mut expected = alloc::vec![top, inner];
expected.sort();
assert_eq!(missing, expected, "both blobs are reported missing");
local.write(ObjectType::Blob, b"top\n").unwrap();
local.write(ObjectType::Blob, b"inner\n").unwrap();
assert!(missing_tree_objects(&local, &root_id).unwrap().is_empty());
let empty = MemoryOdb::new(HashAlgo::Sha1);
empty.write(ObjectType::Tree, &root.serialize()).unwrap();
let missing = missing_tree_objects(&empty, &root_id).unwrap();
assert!(missing.contains(&subtree_id));
assert!(missing.contains(&top));
}
#[test]
fn shallow_walk_truncates_history_and_marks_the_boundary() {
let odb = MemoryOdb::new(HashAlgo::Sha1);
let tree = odb
.write(ObjectType::Tree, &Tree::default().serialize())
.unwrap();
let c1 = commit(&odb, tree, Vec::new());
let c2 = commit_msg(&odb, tree, alloc::vec![c1], b"c2\n");
let c3 = commit_msg(&odb, tree, alloc::vec![c2], b"c3\n");
let none = BTreeSet::new();
let d1 = shallow_objects_to_send(&odb, &[c3], 1, &none).unwrap();
assert!(d1.objects.contains(&c3) && d1.objects.contains(&tree));
assert!(!d1.objects.contains(&c2) && !d1.objects.contains(&c1));
assert_eq!(d1.shallow, BTreeSet::from([c3]));
let d2 = shallow_objects_to_send(&odb, &[c3], 2, &none).unwrap();
assert!(d2.objects.contains(&c3) && d2.objects.contains(&c2));
assert!(!d2.objects.contains(&c1));
assert_eq!(d2.shallow, BTreeSet::from([c2]));
let d3 = shallow_objects_to_send(&odb, &[c3], 3, &none).unwrap();
assert!(d3.objects.contains(&c1));
assert!(d3.shallow.is_empty());
}
#[test]
fn ancestry_and_merge_base() {
let odb = MemoryOdb::new(HashAlgo::Sha1);
let tree = odb
.write(ObjectType::Tree, &Tree::default().serialize())
.unwrap();
let root = commit(&odb, tree, Vec::new());
let a = commit(&odb, tree, alloc::vec![root]);
let b = commit_msg(&odb, tree, alloc::vec![a], b"b\n");
let c = commit_msg(&odb, tree, alloc::vec![a], b"c\n");
assert!(is_ancestor(&odb, &root, &b).unwrap());
assert!(is_ancestor(&odb, &a, &c).unwrap());
assert!(is_ancestor(&odb, &b, &b).unwrap()); assert!(!is_ancestor(&odb, &b, &c).unwrap()); assert!(!is_ancestor(&odb, &c, &a).unwrap());
assert_eq!(merge_base(&odb, &b, &c).unwrap(), Some(a));
assert_eq!(merge_base(&odb, &root, &b).unwrap(), Some(root));
}
#[test]
fn tree_diff_name_status() {
use crate::object::tree::TreeEntry;
let odb = MemoryOdb::new(HashAlgo::Sha1);
let blob1 = odb.write(ObjectType::Blob, b"one\n").unwrap();
let blob2 = odb.write(ObjectType::Blob, b"two\n").unwrap();
let old = odb
.write(
ObjectType::Tree,
&Tree {
entries: alloc::vec![
TreeEntry {
mode: FileMode::Regular,
name: b"keep".to_vec(),
id: blob1
},
TreeEntry {
mode: FileMode::Regular,
name: b"gone".to_vec(),
id: blob1
},
],
}
.serialize(),
)
.unwrap();
let new = odb
.write(
ObjectType::Tree,
&Tree {
entries: alloc::vec![
TreeEntry {
mode: FileMode::Regular,
name: b"keep".to_vec(),
id: blob2
},
TreeEntry {
mode: FileMode::Regular,
name: b"added".to_vec(),
id: blob1
},
],
}
.serialize(),
)
.unwrap();
let diff = diff_trees(&odb, Some(&old), Some(&new)).unwrap();
let find = |p: &[u8]| diff.iter().find(|e| e.path == p).map(|e| e.change);
assert_eq!(find(b"added"), Some(TreeChange::Added));
assert_eq!(find(b"keep"), Some(TreeChange::Modified));
assert_eq!(find(b"gone"), Some(TreeChange::Deleted));
assert_eq!(diff.len(), 3);
}
#[test]
fn revwalk_yields_history() {
let odb = MemoryOdb::new(HashAlgo::Sha1);
let tree = odb
.write(ObjectType::Tree, &Tree::default().serialize())
.unwrap();
let c1 = commit(&odb, tree, Vec::new());
let c2 = commit(&odb, tree, alloc::vec![c1]);
let c3 = commit(&odb, tree, alloc::vec![c2]);
let ids: Vec<ObjectId> = RevWalk::new(&odb, &[c3]).map(|r| r.unwrap().0).collect();
assert_eq!(ids, alloc::vec![c3, c2, c1]);
}
fn commit_at(odb: &MemoryOdb, parents: Vec<ObjectId>, time: i64) -> ObjectId {
let tree = odb.write(ObjectType::Tree, b"").unwrap();
let mut s = sig();
s.time = time;
let c = Commit {
tree,
parents,
author: s.clone(),
committer: s,
extra_headers: Vec::new(),
message: alloc::format!("at {time}\n").into_bytes(),
};
odb.write(ObjectType::Commit, &c.serialize()).unwrap()
}
#[test]
fn merge_base_prefers_the_newest_common_ancestor() {
let odb = MemoryOdb::new(HashAlgo::Sha1);
let o = commit_at(&odb, alloc::vec![], 1);
let a1 = commit_at(&odb, alloc::vec![o], 2);
let a2 = commit_at(&odb, alloc::vec![a1], 3);
let m = commit_at(&odb, alloc::vec![o, a1], 4);
assert_eq!(merge_base(&odb, &a2, &m).unwrap(), Some(a1));
assert_eq!(merge_base(&odb, &m, &a2).unwrap(), Some(a1));
assert_eq!(merge_bases(&odb, &a2, &m).unwrap(), alloc::vec![a1]);
let p1 = commit_at(&odb, alloc::vec![a2], 5);
let q1 = commit_at(&odb, alloc::vec![a2], 6);
let p2 = commit_at(&odb, alloc::vec![p1, q1], 7);
let q2 = commit_at(&odb, alloc::vec![q1, p1], 8);
let mut want = alloc::vec![p1, q1];
want.sort();
assert_eq!(merge_bases(&odb, &p2, &q2).unwrap(), want);
assert_eq!(merge_base(&odb, &p2, &q2).unwrap(), Some(q1));
}
#[test]
fn revwalk_is_date_ordered_like_rev_list() {
let odb = MemoryOdb::new(HashAlgo::Sha1);
let root = commit_at(&odb, alloc::vec![], 10);
let short = commit_at(&odb, alloc::vec![root], 20);
let l1 = commit_at(&odb, alloc::vec![root], 30);
let l2 = commit_at(&odb, alloc::vec![l1], 40);
let l3 = commit_at(&odb, alloc::vec![l2], 50);
let merge = commit_at(&odb, alloc::vec![short, l3], 60);
let ids: Vec<ObjectId> = RevWalk::new(&odb, &[merge]).map(|r| r.unwrap().0).collect();
assert_eq!(ids, alloc::vec![merge, l3, l2, l1, short, root]);
}
}