use std::collections::HashSet;
use std::sync::atomic::AtomicBool;
use super::Objects;
use crate::pack::reader::check_cancelled;
use crate::{ListLimits, ObjectId, ObjectKind, ObjectReadError, ReadLimits};
#[derive(Clone, Debug, Default)]
pub struct PromisedObjects {
ids: HashSet<ObjectId>,
}
impl PromisedObjects {
pub fn contains(&self, id: ObjectId) -> bool {
self.ids.contains(&id)
}
pub fn len(&self) -> usize {
self.ids.len()
}
pub fn is_empty(&self) -> bool {
self.ids.is_empty()
}
}
#[derive(Clone, Debug, PartialEq, Eq, thiserror::Error)]
#[error("{}", describe_missing(.objects))]
#[non_exhaustive]
pub struct MissingObjectsError {
pub objects: Vec<(ObjectId, ObjectKind)>,
}
impl MissingObjectsError {
pub fn new(objects: Vec<(ObjectId, ObjectKind)>) -> Self {
Self { objects }
}
pub fn ids(&self) -> impl ExactSizeIterator<Item = ObjectId> + '_ {
self.objects.iter().map(|&(id, _)| id)
}
}
fn describe_missing(objects: &[(ObjectId, ObjectKind)]) -> String {
let count = objects.len();
let place = "this partial clone";
match objects {
[] => format!("no objects missing from {place}"),
[(id, kind)] => format!("{} {id} is missing from {place}", kind.as_str()),
[(id, kind), ..] => format!(
"{count} objects missing from {place}, first {} {id}",
kind.as_str()
),
}
}
enum Stop {
Read(ObjectReadError),
Malformed,
}
impl From<crate::edges::Error> for Stop {
fn from(_: crate::edges::Error) -> Self {
Self::Malformed
}
}
impl Objects {
pub fn promised_objects(
&self,
limits: ListLimits,
read: ReadLimits,
cancelled: &AtomicBool,
) -> Result<PromisedObjects, ObjectReadError> {
const BATCH: usize = 4096;
let mut promised = PromisedObjects::default();
if !self.partial_clone {
return Ok(promised);
}
let mut remaining = limits.max_entries;
let mut batch = Vec::with_capacity(BATCH);
for pack in self.packs().filter(|pack| pack.is_promisor()) {
let mut ids = pack.ids().peekable();
while ids.peek().is_some() {
batch.clear();
batch.extend(ids.by_ref().take(BATCH));
for (id, header) in self.headers_controlled(batch.drain(..), read, cancelled)? {
check_cancelled(cancelled)?;
if header.is_none_or(|header| header.kind() == ObjectKind::Blob) {
continue;
}
self.collect_references(id, read, cancelled, &mut promised, &mut remaining)?;
}
}
}
Ok(promised)
}
fn collect_references(
&self,
id: ObjectId,
read: ReadLimits,
cancelled: &AtomicBool,
promised: &mut PromisedObjects,
remaining: &mut usize,
) -> Result<(), ObjectReadError> {
let object = self
.read_controlled(id, read, cancelled)?
.ok_or(ObjectReadError::Corrupt("promisor pack object disappeared"))?;
let collected = crate::edges::visit(id, object.kind(), object.data(), |target, _| {
if !promised.ids.contains(&target) {
*remaining = remaining
.checked_sub(1)
.ok_or(Stop::Read(ObjectReadError::Limit("promised objects")))?;
promised.ids.insert(target);
}
Ok::<_, Stop>(())
});
match collected {
Ok(()) => Ok(()),
Err(Stop::Read(error)) => Err(error),
Err(Stop::Malformed) => Err(ObjectReadError::Corrupt("malformed promisor pack object")),
}
}
}
impl crate::Repository {
pub fn is_partial_clone(&self) -> bool {
let config = self.config();
if config.value("extensions", None, "partialClone").is_some() {
return true;
}
let remotes: HashSet<&[u8]> = config
.entries()
.iter()
.filter(|entry| entry.section.eq_ignore_ascii_case(b"remote"))
.filter_map(|entry| entry.subsection.as_deref())
.collect();
remotes.into_iter().any(|remote| {
config
.boolean("remote", Some(remote), "promisor")
.is_ok_and(|promisor| promisor == Some(true))
|| config
.value("remote", Some(remote), "partialCloneFilter")
.is_some()
})
}
}