use std::path::Path;
use std::sync::atomic::{AtomicBool, Ordering};
use crate::fetch::{AdvertisedRef, Advertisement, AdvertisementKind, FetchError};
use crate::refs::{RefName, Reference, ReferenceError, Target};
use crate::{
AlternateLimits, ObjectFormat, ObjectId, OpenLimits, OpenOptions, PackLimits, PeelLimits,
Repository,
};
#[derive(Clone, Copy, Debug)]
#[non_exhaustive]
pub struct LocalAdvertisementLimits {
pub open: OpenLimits,
pub packs: PackLimits,
pub alternates: AlternateLimits,
pub max_ref_entries: usize,
pub max_ref_bytes: usize,
pub max_head_bytes: usize,
pub max_symbolic_depth: usize,
pub reftable: crate::refs::reftable::StackLimits,
pub peel: PeelLimits,
pub max_refs: usize,
pub max_bytes: usize,
}
impl Default for LocalAdvertisementLimits {
fn default() -> Self {
Self {
open: OpenLimits::default(),
packs: PackLimits::default().with_max_index_bytes(512 * 1024 * 1024),
alternates: AlternateLimits::default(),
max_ref_entries: 1_000_000,
max_ref_bytes: 128 * 1024 * 1024,
max_head_bytes: 4096,
max_symbolic_depth: 32,
reftable: crate::refs::reftable::StackLimits::default(),
peel: PeelLimits::default(),
max_refs: 1_000_000,
max_bytes: 128 * 1024 * 1024,
}
}
}
impl LocalAdvertisementLimits {
#[must_use]
pub fn with_open(mut self, limits: OpenLimits) -> Self {
self.open = limits;
self
}
#[must_use]
pub fn with_packs(mut self, limits: PackLimits) -> Self {
self.packs = limits;
self
}
#[must_use]
pub fn with_alternates(mut self, limits: AlternateLimits) -> Self {
self.alternates = limits;
self
}
#[must_use]
pub fn with_max_ref_entries(mut self, value: usize) -> Self {
self.max_ref_entries = value;
self
}
#[must_use]
pub fn with_max_ref_bytes(mut self, value: usize) -> Self {
self.max_ref_bytes = value;
self
}
#[must_use]
pub fn with_max_head_bytes(mut self, value: usize) -> Self {
self.max_head_bytes = value;
self
}
#[must_use]
pub fn with_max_symbolic_depth(mut self, value: usize) -> Self {
self.max_symbolic_depth = value;
self
}
#[must_use]
pub fn with_reftable(mut self, limits: crate::refs::reftable::StackLimits) -> Self {
self.reftable = limits;
self
}
#[must_use]
pub fn with_peel(mut self, limits: PeelLimits) -> Self {
self.peel = limits;
self
}
#[must_use]
pub fn with_max_refs(mut self, value: usize) -> Self {
self.max_refs = value;
self
}
#[must_use]
pub fn with_max_bytes(mut self, value: usize) -> Self {
self.max_bytes = value;
self
}
}
#[derive(Clone, Debug, Default)]
#[non_exhaustive]
pub struct LocalAdvertisementOptions {
limits: LocalAdvertisementLimits,
}
impl LocalAdvertisementOptions {
#[must_use]
pub fn with_limits(mut self, limits: LocalAdvertisementLimits) -> Self {
self.limits = limits;
self
}
pub fn advertise(
&self,
path: impl AsRef<Path>,
cancel: &AtomicBool,
) -> Result<Advertisement, FetchError> {
Ok(admit(path.as_ref(), self.limits, cancel)?.advertisement)
}
}
pub(crate) struct AdmittedSource {
_repository: Repository,
_objects: crate::Objects,
advertisement: Advertisement,
}
impl AdmittedSource {
pub(crate) fn into_parts(self) -> (Repository, crate::Objects, Advertisement) {
(self._repository, self._objects, self.advertisement)
}
}
pub(crate) fn admit(
path: &Path,
limits: LocalAdvertisementLimits,
cancel: &AtomicBool,
) -> Result<AdmittedSource, FetchError> {
let mut poll = Poll::new(cancel)?;
let repository = OpenOptions::default()
.with_limits(limits.open)
.open(path, cancel)
.map_err(|error| FetchError::Source(Box::new(error)))?;
if repository.git_dir() != repository.common_dir() {
return Err(FetchError::LinkedWorktrees);
}
match std::fs::read_dir(repository.common_dir().join("worktrees")) {
Ok(mut entries) => {
if let Some(entry) = entries.next() {
entry?;
return Err(FetchError::LinkedWorktrees);
}
}
Err(error) if error.kind() == std::io::ErrorKind::NotFound => {}
Err(error) => return Err(FetchError::Io(error)),
}
let objects = repository
.objects_controlled(limits.packs, limits.alternates, cancel)
.map_err(FetchError::SourceObjects)?;
let references = repository
.references()
.map_err(refs_error)?
.with_reftable_limits(limits.reftable);
let listed = references
.list_controlled(limits.max_ref_entries, limits.max_ref_bytes, cancel)
.map_err(refs_error)?;
let head = references
.head_controlled(limits.max_head_bytes, cancel)
.map_err(refs_error)?;
let mut output = Output {
refs: Vec::new(),
remaining: limits.max_bytes,
maximum: limits.max_refs,
};
if let Some(id) = resolve(
head.as_ref(),
head.as_ref(),
&listed,
limits.max_symbolic_depth,
&mut poll,
)? {
output.push(&RefName::HEAD, id, false)?;
}
for reference in &listed {
poll.step()?;
let Some(id) = resolve(
Some(&reference.target),
head.as_ref(),
&listed,
limits.max_symbolic_depth,
&mut poll,
)?
else {
continue;
};
output.push(&reference.name, id, false)?;
if reference.name.as_bytes().starts_with(b"refs/tags/") {
let peeled = objects
.peel(id, limits.peel, cancel)
.map_err(|error| FetchError::Peel(Box::new(error)))?;
if !peeled.tags.is_empty() {
output.push(&reference.name, peeled.target, true)?;
}
}
}
let mut capabilities = Vec::new();
if let Some(Target::Symbolic(name)) = &head
&& name.as_bytes().starts_with(b"refs/heads/")
{
output.charge(b"symref=HEAD:".len())?;
output.charge(name.as_bytes().len())?;
output.charge(1)?;
let mut hint = b"symref=HEAD:".to_vec();
hint.extend_from_slice(name.as_bytes());
capabilities.push(hint);
}
if repository.object_format() == ObjectFormat::Sha256 {
output.charge(b"object-format=sha256".len() + 1)?;
capabilities.push(b"object-format=sha256".to_vec());
}
check(cancel)?;
let advertisement = Advertisement::new(output.refs, capabilities);
Ok(AdmittedSource {
_repository: repository,
_objects: objects,
advertisement,
})
}
fn refs_error(error: ReferenceError) -> FetchError {
FetchError::LocalReferences(Box::new(error))
}
fn resolve<'a>(
mut target: Option<&'a Target>,
head: Option<&'a Target>,
listed: &'a [Reference],
maximum: usize,
poll: &mut Poll<'_>,
) -> Result<Option<ObjectId>, FetchError> {
for depth in 0..=maximum {
poll.step()?;
match target {
None => return Ok(None),
Some(Target::Direct(id)) => return Ok(Some(*id)),
Some(Target::Symbolic(_)) if depth == maximum => {
return Err(refs_error(ReferenceError::Depth(maximum)));
}
Some(Target::Symbolic(next)) => {
target = if next == &RefName::HEAD {
head
} else {
listed
.binary_search_by(|reference| reference.name.cmp(next))
.ok()
.and_then(|index| listed.get(index))
.map(|reference| &reference.target)
}
}
}
}
Err(refs_error(ReferenceError::Depth(maximum)))
}
struct Output {
refs: Vec<AdvertisedRef>,
remaining: usize,
maximum: usize,
}
impl Output {
fn charge(&mut self, bytes: usize) -> Result<(), FetchError> {
self.remaining = self
.remaining
.checked_sub(bytes)
.ok_or(FetchError::Limit("local advertisement bytes"))?;
Ok(())
}
fn push(&mut self, name: &RefName, id: ObjectId, peeled: bool) -> Result<(), FetchError> {
if self.refs.len() >= self.maximum {
return Err(FetchError::Limit("local references"));
}
self.charge(name.as_bytes().len())?;
self.charge(id.as_bytes().len() * 2 + 8)?;
self.refs.push(AdvertisedRef::new(
name.clone(),
id,
if peeled {
AdvertisementKind::PeeledHint
} else {
AdvertisementKind::Tip
},
));
Ok(())
}
}
struct Poll<'a> {
cancel: &'a AtomicBool,
remaining: usize,
}
impl<'a> Poll<'a> {
fn new(cancel: &'a AtomicBool) -> Result<Self, FetchError> {
check(cancel)?;
Ok(Self {
cancel,
remaining: 64,
})
}
fn step(&mut self) -> Result<(), FetchError> {
self.remaining -= 1;
if self.remaining == 0 {
check(self.cancel)?;
self.remaining = 64;
}
Ok(())
}
}
fn check(cancel: &AtomicBool) -> Result<(), FetchError> {
if cancel.load(Ordering::Relaxed) {
Err(FetchError::Cancelled)
} else {
Ok(())
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn cancellation_is_polled_at_the_batch_boundary() {
let cancel = AtomicBool::new(false);
let mut poll = Poll::new(&cancel).unwrap();
for _ in 0..63 {
poll.step().unwrap();
}
cancel.store(true, Ordering::Relaxed);
assert!(matches!(poll.step(), Err(FetchError::Cancelled)));
}
}