use std::collections::{HashSet, VecDeque};
use std::sync::atomic::AtomicBool;
use super::{Objects, PromisedObjects};
use crate::pack::reader::check_cancelled;
use crate::{ListLimits, ObjectId, ObjectKind, ObjectReadError, ReadLimits};
#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
#[non_exhaustive]
pub enum MissingObjects {
#[default]
Error,
AllowPromisor,
AllowAny,
}
#[derive(Clone, Copy, Debug)]
#[non_exhaustive]
pub struct ObjectWalkLimits {
pub max_objects: usize,
pub read: ReadLimits,
pub promised: ListLimits,
}
impl ObjectWalkLimits {
#[must_use]
pub fn with_max_objects(mut self, max_objects: usize) -> Self {
self.max_objects = max_objects;
self
}
#[must_use]
pub fn with_read(mut self, read: ReadLimits) -> Self {
self.read = read;
self
}
#[must_use]
pub fn with_promised(mut self, promised: ListLimits) -> Self {
self.promised = promised;
self
}
}
impl Default for ObjectWalkLimits {
fn default() -> Self {
Self {
max_objects: 1 << 27,
read: ReadLimits::default(),
promised: ListLimits::default(),
}
}
}
#[derive(Clone, Debug, Default, Eq, PartialEq)]
#[non_exhaustive]
pub struct ObjectWalk {
pub objects: Vec<(ObjectId, ObjectKind)>,
pub missing: Vec<(ObjectId, ObjectKind)>,
}
#[derive(Debug, thiserror::Error)]
#[non_exhaustive]
pub enum ObjectWalkError {
#[error("missing tip object {0}")]
MissingTip(ObjectId),
#[error("missing {} {id}", kind.as_str())]
Missing {
id: ObjectId,
kind: ObjectKind,
},
#[error("{id} is a {} but is referenced as a {}", actual.as_str(), expected.as_str())]
WrongKind {
id: ObjectId,
expected: ObjectKind,
actual: ObjectKind,
},
#[error("malformed {} {id}", kind.as_str())]
Malformed {
id: ObjectId,
kind: ObjectKind,
},
#[error("object walk limit exceeded")]
Limit,
#[error(transparent)]
Read(#[from] ObjectReadError),
}
impl From<crate::edges::Error> for ObjectWalkError {
fn from(error: crate::edges::Error) -> Self {
let (id, kind) = match error {
crate::edges::Error::Commit { id, .. } => (id, ObjectKind::Commit),
crate::edges::Error::Tree { id, .. } => (id, ObjectKind::Tree),
crate::edges::Error::Tag { id, .. } => (id, ObjectKind::Tag),
};
Self::Malformed { id, kind }
}
}
impl Objects {
pub fn walk_objects(
&self,
tips: &[ObjectId],
missing: MissingObjects,
limits: ObjectWalkLimits,
cancelled: &AtomicBool,
) -> Result<ObjectWalk, ObjectWalkError> {
let mut walk = ObjectWalk::default();
let mut seen = HashSet::new();
let mut pending = VecDeque::new();
let mut promised: Option<PromisedObjects> = None;
let enqueue = |seen: &mut HashSet<ObjectId>,
pending: &mut VecDeque<(ObjectId, Option<ObjectKind>, bool)>,
entry: (ObjectId, Option<ObjectKind>, bool)|
-> Result<(), ObjectWalkError> {
check_cancelled(cancelled)?;
if seen.contains(&entry.0) {
return Ok(());
}
if seen.len() >= limits.max_objects {
return Err(ObjectWalkError::Limit);
}
seen.insert(entry.0);
pending.push_back(entry);
Ok(())
};
for &tip in tips {
enqueue(&mut seen, &mut pending, (tip, None, false))?;
}
while let Some((id, expected, parent)) = pending.pop_front() {
check_cancelled(cancelled)?;
let stored = if expected == Some(ObjectKind::Blob) {
self.header(id, limits.read)?
.map(|header| (header.kind(), None))
} else {
self.read_controlled(id, limits.read, cancelled)?
.map(|object| (object.kind(), Some(object)))
};
let Some((kind, object)) = stored else {
let Some(kind) = expected else {
return Err(ObjectWalkError::MissingTip(id));
};
let allowed = match missing {
MissingObjects::Error => false,
MissingObjects::AllowAny => true,
MissingObjects::AllowPromisor if parent => false,
MissingObjects::AllowPromisor => {
let promised = match &mut promised {
Some(promised) => promised,
empty => empty.insert(self.promised_objects(
limits.promised,
limits.read,
cancelled,
)?),
};
promised.contains(id)
}
};
if !allowed {
return Err(ObjectWalkError::Missing { id, kind });
}
walk.missing.push((id, kind));
continue;
};
if let Some(expected) = expected.filter(|expected| *expected != kind) {
return Err(ObjectWalkError::WrongKind {
id,
expected,
actual: kind,
});
}
walk.objects.push((id, kind));
let Some(object) = object else {
continue;
};
let shallow = kind == ObjectKind::Commit && self.shallow_roots().contains(id);
crate::edges::visit(id, kind, object.data(), |target, target_kind| {
let parent = kind == ObjectKind::Commit && target_kind == ObjectKind::Commit;
if parent && shallow {
return Ok(());
}
enqueue(&mut seen, &mut pending, (target, Some(target_kind), parent))
})?;
}
Ok(walk)
}
}