use std::collections::BTreeSet;
use std::sync::atomic::AtomicBool;
use super::{FetchError, FetchLimits, FetchUpdate, FetchUpdateKind, check_cancelled};
use crate::refs::RefName;
use crate::{HistoryLimits, ObjectId, ObjectKind, PackLimits, PeelLimits};
#[derive(Clone, Copy, Debug)]
pub struct FetchUpdateLimits {
pub snapshot: PackLimits,
pub verification: FetchLimits,
pub history: HistoryLimits,
pub max_tag_depth: usize,
}
impl Default for FetchUpdateLimits {
fn default() -> Self {
Self {
snapshot: PackLimits::default(),
verification: FetchLimits::default(),
history: HistoryLimits::default(),
max_tag_depth: 32,
}
}
}
impl FetchUpdateLimits {
pub const fn trusted() -> Self {
Self {
snapshot: PackLimits::trusted(),
verification: FetchLimits::trusted(),
history: HistoryLimits::trusted(),
max_tag_depth: 10_000,
}
}
}
#[derive(Debug, thiserror::Error)]
pub enum FetchUpdateError {
#[error(transparent)]
Object(#[from] FetchError),
#[error(transparent)]
Peel(#[from] crate::PeelError),
#[error(transparent)]
History(#[from] crate::HistoryError),
#[error("non-fast-forward fetch requires force intent and authorization: {0:?}")]
NonFastForward(RefName),
}
pub(super) fn validate(
updates: &mut [FetchUpdate],
verified: &super::verified::InstalledObjects<'_>,
authorized: &BTreeSet<RefName>,
limits: FetchUpdateLimits,
cancel: &AtomicBool,
) -> Result<(), FetchUpdateError> {
let objects = verified.store();
for update in updates {
check_cancelled(cancel)?;
if update.kind != FetchUpdateKind::Replace {
continue;
}
let old = update.previous.expect("replacement has old value");
let new = update.mapping.source.as_ref().expect("fetch source").id;
let old_commit = peel_commit(verified, old, limits, cancel)?;
let new_commit = peel_commit(verified, new, limits, cancel)?;
let (Some(old), Some(new)) = (old_commit, new_commit) else {
continue;
};
let fast_forward =
objects.is_ancestor_verified(old, new, limits.history, cancel, |id, read| {
let Some(object) = verified
.get(id, read, cancel)
.map_err(|source| crate::HistoryError::Read { id, source })?
else {
return Ok(None);
};
if object.kind != ObjectKind::Commit {
return Err(crate::HistoryError::NotCommit(id));
}
let parents = object
.edges()
.expect("InstalledObjects::get requires parsed edges")
.iter()
.filter_map(|&(id, kind)| (kind == ObjectKind::Commit).then_some(id))
.collect();
Ok(Some(parents))
})?;
check_cancelled(cancel)?;
let name = update
.mapping
.destination
.as_ref()
.expect("replacement destination");
update.kind = commit_update_kind(
name,
fast_forward,
update.mapping.force,
authorized.contains(name),
)?;
}
Ok(())
}
fn commit_update_kind(
name: &RefName,
fast_forward: bool,
force: bool,
authorized: bool,
) -> Result<FetchUpdateKind, FetchUpdateError> {
if fast_forward {
Ok(FetchUpdateKind::FastForward)
} else if force && authorized {
Ok(FetchUpdateKind::ForcedTracking)
} else {
Err(FetchUpdateError::NonFastForward(name.clone()))
}
}
fn peel_commit(
verified: &super::verified::InstalledObjects<'_>,
id: ObjectId,
limits: FetchUpdateLimits,
cancel: &AtomicBool,
) -> Result<Option<ObjectId>, FetchUpdateError> {
let mut read = limits.history.read;
read.max_object_bytes = read
.max_object_bytes
.min(limits.verification.max_known_bytes);
if let Some(object) = verified
.get(id, read, cancel)
.map_err(|source| FetchError::LocalRead { id, source })?
&& object.kind != ObjectKind::Tag
{
return Ok((object.kind == ObjectKind::Commit).then_some(id));
}
let result = verified.store().peel(
id,
PeelLimits {
max_tags: limits.max_tag_depth,
max_bytes: limits.verification.max_known_bytes,
read: limits.history.read,
},
cancel,
)?;
Ok((result.kind == ObjectKind::Commit).then_some(result.target))
}
#[cfg(test)]
mod tests {
use rstest::rstest;
use super::*;
#[rstest]
#[case::fast_forward(true, false, false, FetchUpdateKind::FastForward)]
#[case::forced(false, true, true, FetchUpdateKind::ForcedTracking)]
fn commit_decisions(
#[case] ff: bool,
#[case] force: bool,
#[case] auth: bool,
#[case] expected: FetchUpdateKind,
) {
let name = RefName::new("refs/remotes/origin/main").unwrap();
assert_eq!(
commit_update_kind(&name, ff, force, auth).unwrap(),
expected
);
}
#[rstest]
#[case::neither(false, false)]
#[case::syntax_only(true, false)]
#[case::authorization_only(false, true)]
fn reject_unauthorized_rewind(#[case] force: bool, #[case] auth: bool) {
let name = RefName::new("refs/remotes/origin/main").unwrap();
assert!(matches!(
commit_update_kind(&name, false, force, auth),
Err(FetchUpdateError::NonFastForward(_))
));
}
}