use std::collections::{BTreeMap, BTreeSet, VecDeque};
use std::fs;
use std::io::Read;
use std::path::PathBuf;
use std::sync::atomic::{AtomicBool, Ordering};
use std::time::SystemTime;
use crate::refs::{ImportedRecord, RefName, ReflogLimits, Target};
use crate::{
Commit, IdMap, IdSet, Object, ObjectId, ObjectKind, PackLimits, ReadLimits, Repository, Tag,
Tree, index,
};
mod error;
pub use error::{RetentionError, RetentionErrorKind};
mod repack;
pub use repack::{RepackError, RepackLimits, RepackPublished};
mod prune;
pub use prune::{MaintenanceIsolation, PruneCause, PruneFailure, PruneReport};
mod config;
pub use config::{ReflogCutoffs, ReflogExpiry, ReflogExpiryError};
mod expire;
pub use expire::{ExpireCause, ExpireFailure, ExpireReport};
mod retire;
pub use retire::{RetireCause, RetireFailure, RetireReport};
mod compose;
mod inputs;
#[cfg(test)]
mod walk_tests;
pub use compose::{MaintenanceCause, MaintenanceFailure, MaintenanceReport};
#[derive(Clone, Debug, Eq, PartialEq)]
struct ObservedLog {
source: ReflogSource,
position: usize,
seconds: i128,
old: ObjectId,
new: ObjectId,
}
#[derive(Clone, Debug, Eq, Ord, PartialEq, PartialOrd)]
pub struct ReflogSource {
root: PathBuf,
name: RefName,
}
impl ReflogSource {
pub fn new(root: impl Into<PathBuf>, name: RefName) -> Self {
Self {
root: root.into(),
name,
}
}
pub fn root(&self) -> &std::path::Path {
&self.root
}
pub fn name(&self) -> &RefName {
&self.name
}
}
#[derive(Clone, Debug, Default)]
#[non_exhaustive]
pub enum ReflogExpiryPolicy {
#[default]
KeepAll,
ClassifiedCutoffs {
live_before: Option<i128>,
disconnected_before: Option<i128>,
},
Configured(ReflogExpiry),
}
#[derive(Clone, Debug)]
#[non_exhaustive]
pub struct RetentionPolicy {
pub heads: Vec<ObjectId>,
pub recent_cutoff: Option<SystemTime>,
pub reflog_expiry: ReflogExpiryPolicy,
pub max_entries: usize,
pub max_edges: usize,
pub max_bytes: u64,
pub reflog: ReflogLimits,
pub reftable: crate::refs::reftable::StackLimits,
pub max_reference_bytes: usize,
pub max_reflog_bytes: u64,
pub index: index::Limits,
pub packs: PackLimits,
pub read: ReadLimits,
}
impl RetentionPolicy {
#[must_use]
pub fn with_heads(mut self, heads: Vec<ObjectId>) -> Self {
self.heads = heads;
self
}
#[must_use]
pub fn with_recent_cutoff(mut self, recent_cutoff: SystemTime) -> Self {
self.recent_cutoff = Some(recent_cutoff);
self
}
#[must_use]
pub fn with_reflog_expiry(mut self, reflog_expiry: ReflogExpiryPolicy) -> Self {
self.reflog_expiry = reflog_expiry;
self
}
#[must_use]
pub fn with_max_entries(mut self, max_entries: usize) -> Self {
self.max_entries = max_entries;
self
}
#[must_use]
pub fn with_max_edges(mut self, max_edges: usize) -> Self {
self.max_edges = max_edges;
self
}
#[must_use]
pub fn with_max_bytes(mut self, max_bytes: u64) -> Self {
self.max_bytes = max_bytes;
self
}
#[must_use]
pub fn with_reflog(mut self, reflog: ReflogLimits) -> Self {
self.reflog = reflog;
self
}
#[must_use]
pub fn with_reftable(mut self, reftable: crate::refs::reftable::StackLimits) -> Self {
self.reftable = reftable;
self
}
#[must_use]
pub fn with_max_reference_bytes(mut self, max_reference_bytes: usize) -> Self {
self.max_reference_bytes = max_reference_bytes;
self
}
#[must_use]
pub fn with_max_reflog_bytes(mut self, max_reflog_bytes: u64) -> Self {
self.max_reflog_bytes = max_reflog_bytes;
self
}
#[must_use]
pub fn with_index(mut self, index: index::Limits) -> Self {
self.index = index;
self
}
#[must_use]
pub fn with_packs(mut self, packs: PackLimits) -> Self {
self.packs = packs;
self
}
#[must_use]
pub fn with_read(mut self, read: ReadLimits) -> Self {
self.read = read;
self
}
}
impl Default for RetentionPolicy {
fn default() -> Self {
Self {
heads: Vec::new(),
recent_cutoff: None,
reflog_expiry: ReflogExpiryPolicy::KeepAll,
max_entries: 1_000_000,
max_edges: 4_000_000,
max_bytes: 4 * 1024 * 1024 * 1024,
reflog: ReflogLimits::default(),
reftable: crate::refs::reftable::StackLimits::default(),
max_reference_bytes: 64 * 1024 * 1024,
max_reflog_bytes: 64 * 1024 * 1024,
index: index::Limits::default(),
packs: PackLimits::default(),
read: ReadLimits::default(),
}
}
}
impl RetentionPolicy {
pub fn trusted() -> Self {
Self {
max_entries: usize::MAX / 4,
max_edges: usize::MAX / 4,
max_bytes: u64::MAX / 4,
reflog: ReflogLimits::trusted(),
reftable: crate::refs::reftable::StackLimits::trusted(),
max_reference_bytes: usize::MAX / 4,
max_reflog_bytes: u64::MAX / 4,
index: index::Limits::trusted(),
packs: PackLimits::trusted(),
read: ReadLimits::trusted(),
..Self::default()
}
}
}
#[derive(Debug)]
#[non_exhaustive]
pub struct RetentionPlan {
pub roots: BTreeSet<ObjectId>,
pub strong_roots: BTreeSet<ObjectId>,
pub required: BTreeSet<ObjectId>,
pub reflog_expiry_candidates: BTreeMap<ReflogSource, Vec<usize>>,
observed_logs: Vec<ObservedLog>,
store: Option<crate::Objects>,
object_info: BTreeMap<ObjectId, ObjectInfo>,
protected_objects: BTreeSet<ObjectId>,
pub reachable: BTreeSet<ObjectId>,
pub protected_packs: BTreeSet<PathBuf>,
pub alternate_stores: Vec<PathBuf>,
pub outcome: RetentionOutcome,
}
#[derive(Debug)]
struct ObjectInfo {
kind: ObjectKind,
size: u64,
}
#[derive(Debug)]
#[non_exhaustive]
pub enum RetentionOutcome {
Complete,
Incomplete(RetentionError),
}
impl RetentionPlan {
pub fn is_complete(&self) -> bool {
matches!(self.outcome, RetentionOutcome::Complete)
}
fn check_packs_unchanged(&self, cancel: &AtomicBool) -> Result<(), crate::ObjectReadError> {
if let Some(store) = &self.store {
store.check_packs_unchanged(cancel)?;
}
Ok(())
}
fn fail(&mut self, reason: RetentionError) {
self.outcome = RetentionOutcome::Incomplete(reason);
}
}
impl Repository {
fn observe_retention_roots(
&self,
policy: &RetentionPolicy,
cancel: &AtomicBool,
) -> RetentionPlan {
let mut plan = RetentionPlan {
roots: policy.heads.iter().copied().collect(),
strong_roots: policy.heads.iter().copied().collect(),
required: BTreeSet::new(),
reflog_expiry_candidates: BTreeMap::new(),
observed_logs: Vec::new(),
store: None,
object_info: BTreeMap::new(),
protected_objects: BTreeSet::new(),
reachable: BTreeSet::new(),
protected_packs: BTreeSet::new(),
alternate_stores: Vec::new(),
outcome: RetentionOutcome::Complete,
};
if policy
.heads
.iter()
.any(|id| id.format() != self.object_format() || id.is_null())
{
plan.fail(RetentionError::InvalidCallerHead);
return plan;
}
if policy.heads.len() > policy.max_entries {
plan.fail(RetentionError::CallerHeadLimit);
return plan;
}
if let Err(reason) = collect_roots(self, policy, cancel, &mut plan) {
plan.fail(reason);
return plan;
}
plan
}
pub fn plan_retention(&self, policy: &RetentionPolicy, cancel: &AtomicBool) -> RetentionPlan {
let _headers = crate::pack::HeaderScope::new();
let mut plan = self.observe_retention_roots(policy, cancel);
if !plan.is_complete() {
return plan;
}
let store = match self.objects_controlled(
policy.packs,
crate::AlternateLimits::default(),
cancel,
) {
Ok(store) => store,
Err(error) => {
plan.fail(RetentionError::read(error));
return plan;
}
};
if let Err(error) = store.verify_indexes(cancel) {
plan.fail(RetentionError::read(error));
return plan;
}
plan.alternate_stores = store
.store_directories()
.skip(1)
.map(PathBuf::from)
.collect();
let roots = plan.roots.clone();
let strong_roots = plan.strong_roots.clone();
match walk(self, &store, policy, cancel, &roots, &mut plan.object_info) {
Ok(reachable) => plan.reachable = reachable,
Err(reason) => {
plan.fail(reason);
return plan;
}
}
let live = if strong_roots == roots {
plan.reachable.clone()
} else {
match walk(
self,
&store,
policy,
cancel,
&strong_roots,
&mut plan.object_info,
) {
Ok(reachable) => reachable,
Err(reason) => {
plan.fail(reason);
return plan;
}
}
};
let mut required_roots = strong_roots.clone();
for record in &plan.observed_logs {
let expires = match &policy.reflog_expiry {
ReflogExpiryPolicy::KeepAll => false,
ReflogExpiryPolicy::ClassifiedCutoffs {
live_before,
disconnected_before,
} => {
let live_record = live.contains(&record.old) || live.contains(&record.new);
let cutoff = if live_record {
live_before
} else {
disconnected_before
};
cutoff.is_some_and(|cutoff| record.seconds < cutoff)
}
ReflogExpiryPolicy::Configured(expiry) => {
let cutoffs = match expiry.cutoffs(record.source.name()) {
Ok(cutoffs) => cutoffs,
Err(error) => {
plan.fail(RetentionError::read(error));
return plan;
}
};
let live_record = (record.old.is_null() || live.contains(&record.old))
&& (record.new.is_null() || live.contains(&record.new));
cutoffs
.general
.is_some_and(|cutoff| record.seconds < cutoff)
|| (!live_record
&& cutoffs
.unreachable
.is_some_and(|cutoff| record.seconds < cutoff))
}
};
if expires {
plan.reflog_expiry_candidates
.entry(record.source.clone())
.or_default()
.push(record.position);
} else {
required_roots.extend(
[record.old, record.new]
.into_iter()
.filter(|id| !id.is_null()),
);
}
}
if required_roots == roots {
plan.required = plan.reachable.clone();
} else if required_roots == strong_roots {
plan.required = live;
} else {
match walk(
self,
&store,
policy,
cancel,
&required_roots,
&mut plan.object_info,
) {
Ok(required) => plan.required = required,
Err(reason) => plan.fail(reason),
}
}
let has_candidates = match prune::candidates(self, policy, cancel) {
Ok(candidates) => !candidates.is_empty(),
Err(error) => {
plan.fail(RetentionError::from_prune(error));
return plan;
}
};
let primary_packs = store.primary_packs_with_paths();
let has_old_pack = primary_packs
.into_iter()
.any(|(path, _)| !plan.protected_packs.contains(path));
if (has_candidates || has_old_pack) && !plan.protected_packs.is_empty() {
let indexed: BTreeSet<_> = store
.primary_packs_with_paths()
.into_iter()
.map(|(path, _)| path.to_path_buf())
.collect();
for path in &plan.protected_packs {
if indexed.contains(path) {
continue;
}
match fs::symlink_metadata(path) {
Err(error) if error.kind() == std::io::ErrorKind::NotFound => {}
Err(error) => {
plan.fail(RetentionError::read(error));
return plan;
}
Ok(_) => {
plan.fail(RetentionError::MissingProtectedIndex);
return plan;
}
}
}
if let Err(error) = store.verify_packs(cancel) {
plan.fail(RetentionError::read(error));
return plan;
}
let mut protected_roots = BTreeSet::new();
for (path, pack) in store.primary_packs_with_paths() {
if !plan.protected_packs.contains(path) {
continue;
}
for id in pack.identities() {
let id = match id {
Ok(id) => id,
Err(error) => {
plan.fail(RetentionError::read(error));
return plan;
}
};
let (kind, size) = match store.locate(id, policy.read, cancel) {
Ok(Some(info)) => info,
Ok(None) => {
plan.fail(RetentionError::MissingProtectedObject);
return plan;
}
Err(error) => {
plan.fail(RetentionError::read(error));
return plan;
}
};
plan.protected_objects.insert(id);
if kind == ObjectKind::Blob {
plan.object_info
.entry(id)
.or_insert(ObjectInfo { kind, size });
} else {
protected_roots.insert(id);
}
if plan.protected_objects.len() > policy.max_entries {
plan.fail(RetentionError::ProtectedPackedObjectLimit);
return plan;
}
}
}
match walk(
self,
&store,
policy,
cancel,
&protected_roots,
&mut plan.object_info,
) {
Ok(objects) => plan.protected_objects.extend(objects),
Err(reason) => plan.fail(reason),
}
}
if plan.is_complete() {
let is_blob = |id| {
plan.object_info
.get(&id)
.is_some_and(|info| info.kind == ObjectKind::Blob)
};
if let Err(error) =
store.check_retention_blob_kinds(policy.read, policy.max_entries, cancel, is_blob)
{
plan.fail(RetentionError::read(error));
}
}
if let Err(error) = store.check_packs_unchanged(cancel) {
plan.fail(RetentionError::read(error));
}
plan.store = Some(store);
plan
}
}
fn collect_roots(
repository: &Repository,
policy: &RetentionPolicy,
cancel: &AtomicBool,
plan: &mut RetentionPlan,
) -> Result<(), RetentionError> {
if repository
.config()
.value("gc", None, "recentObjectsHook")
.is_some()
{
return Err(RetentionError::RecentObjectsHook);
}
let mut repositories =
vec![Repository::open(repository.common_dir()).map_err(RetentionError::read)?];
let mut reflog_bytes = 0u64;
let mut seen_logs = BTreeSet::new();
let linked = repository
.worktrees(policy.max_entries, cancel)
.map_err(RetentionError::read)?;
for entry in linked {
check(cancel)?;
let retained = repository
.open_retained_worktree(&entry.git_dir)
.map_err(RetentionError::read)?;
repositories.push(retained);
}
for repo in repositories {
check(cancel)?;
if repo
.config()
.value("extensions", None, "preciousObjects")
.is_some()
{
return Err(RetentionError::PreciousObjects);
}
if repo
.config()
.value("extensions", None, "partialClone")
.is_some()
{
return Err(RetentionError::PartialClone);
}
if repo
.config()
.value("gc", None, "recentObjectsHook")
.is_some()
{
return Err(RetentionError::RecentObjectsHook);
}
let refs = repo
.references()
.map_err(RetentionError::read)?
.with_reftable_limits(policy.reftable);
let references = refs
.list_controlled(policy.max_entries, policy.max_reference_bytes, cancel)
.map_err(RetentionError::read)?;
let mut values: BTreeMap<_, _> = references
.into_iter()
.map(|reference| (reference.name, reference.target))
.collect();
if let Some(head) = refs
.head_controlled(policy.max_reference_bytes, cancel)
.map_err(RetentionError::read)?
{
values.insert(RefName::HEAD, head);
}
for target in values.values() {
if let Some(id) = resolve_target(target, &values)? {
plan.roots.insert(id);
plan.strong_roots.insert(id);
}
}
for name in refs
.imported_reflog_names(policy.max_entries, cancel)
.map_err(RetentionError::read)?
{
let root = if name.per_worktree() {
repo.git_dir()
} else {
repo.common_dir()
};
let source = ReflogSource::new(root, name.clone());
if !seen_logs.insert(source.clone()) {
continue;
}
let log = refs
.imported_reflog(&name, policy.reflog, cancel)
.map_err(RetentionError::read)?
.ok_or_else(|| RetentionError::ReflogDisappeared(name.clone()))?;
plan.roots.extend(log.recoverable_roots());
for (position, record) in log.records().iter().enumerate() {
let size = match record {
ImportedRecord::File { bytes, .. } => bytes.len(),
ImportedRecord::Reftable { value, .. } => value
.name
.len()
.saturating_add(value.email.len())
.saturating_add(value.message.len())
.saturating_add(2 * repo.object_format().digest_len() + 18),
};
reflog_bytes = reflog_bytes.saturating_add(size as u64);
if reflog_bytes > policy.max_reflog_bytes {
return Err(RetentionError::AggregateReflogByteLimit);
}
if plan.observed_logs.len() >= policy.max_entries {
return Err(RetentionError::AggregateReflogRecordLimit);
}
if let Ok(fields) = record.fields() {
plan.observed_logs.push(ObservedLog {
source: source.clone(),
position: position + 1,
seconds: fields.seconds,
old: fields.old,
new: fields.new,
});
}
}
if !log.is_complete() {
return Err(RetentionError::reflog(log));
}
}
collect_pseudorefs(&repo, policy, plan)?;
if let Some(index) = repo
.read_index(policy.index)
.map_err(RetentionError::read)?
{
if index.entries().len() > policy.max_entries {
return Err(RetentionError::IndexEntryLimit);
}
for entry in index.entries() {
if !entry.intent_to_add && entry.mode != index::Mode::Gitlink && !entry.id.is_null()
{
plan.roots.insert(entry.id);
plan.strong_roots.insert(entry.id);
}
}
}
if plan.roots.len() > policy.max_entries {
return Err(RetentionError::AggregateRootLimit);
}
}
recent_objects(repository, policy, cancel, plan)?;
Ok(())
}
fn resolve_target(
target: &Target,
values: &BTreeMap<RefName, Target>,
) -> Result<Option<ObjectId>, RetentionError> {
let mut target = target;
let mut visited = BTreeSet::new();
for _ in 0..=32 {
match target {
Target::Direct(id) => return Ok(Some(*id)),
Target::Symbolic(name) => {
if !visited.insert(name) {
return Err(RetentionError::SymbolicReferenceCycle);
}
let Some(next) = values.get(name) else {
return Ok(None);
};
target = next;
}
}
}
Err(RetentionError::SymbolicReferenceDepthLimit)
}
fn collect_pseudorefs(
repo: &Repository,
policy: &RetentionPolicy,
plan: &mut RetentionPlan,
) -> Result<(), RetentionError> {
for name in [
"ORIG_HEAD",
"MERGE_HEAD",
"CHERRY_PICK_HEAD",
"REVERT_HEAD",
"BISECT_HEAD",
"REBASE_HEAD",
"AUTO_MERGE",
"FETCH_HEAD",
] {
let path = repo.git_dir().join(name);
let file = match fs::File::open(&path) {
Ok(file) => file,
Err(error) if error.kind() == std::io::ErrorKind::NotFound => continue,
Err(error) => {
return Err(RetentionError::PseudorefIo {
path: path.clone(),
source: error,
});
}
};
let mut bytes = Vec::new();
file.take(policy.reflog.bytes as u64 + 1)
.read_to_end(&mut bytes)
.map_err(RetentionError::read)?;
if bytes.len() > policy.reflog.bytes {
return Err(RetentionError::PseudorefByteLimit(path.clone()));
}
for line in bytes
.split(|byte| *byte == b'\n')
.filter(|line| !line.is_empty())
{
let width = repo.object_format().digest_len() * 2;
let id = line
.get(..width)
.and_then(|value| std::str::from_utf8(value).ok())
.and_then(|value| ObjectId::from_hex(repo.object_format(), value).ok())
.ok_or_else(|| RetentionError::MalformedPseudoref(path.clone()))?;
if line.len() > width && !matches!(line[width], b'\t' | b' ' | b'\r') {
return Err(RetentionError::MalformedPseudoref(path.clone()));
}
if !id.is_null() {
plan.roots.insert(id);
plan.strong_roots.insert(id);
}
if plan.roots.len() > policy.max_entries {
return Err(RetentionError::AggregateRootLimit);
}
}
}
Ok(())
}
fn recent_objects(
repo: &Repository,
policy: &RetentionPolicy,
cancel: &AtomicBool,
plan: &mut RetentionPlan,
) -> Result<(), RetentionError> {
let mut entries = 0;
for dir in fs::read_dir(repo.object_dir()).map_err(RetentionError::read)? {
check(cancel)?;
entries += 1;
if entries > policy.max_entries {
return Err(RetentionError::ObjectDirectoryEntryLimit);
}
let dir = dir.map_err(RetentionError::read)?;
if !fs::symlink_metadata(dir.path())
.map_err(RetentionError::read)?
.is_dir()
{
return Err(RetentionError::NonDirectoryObjectEntry);
}
let name = dir.file_name();
if name == "pack" {
for file in fs::read_dir(dir.path()).map_err(RetentionError::read)? {
entries += 1;
if entries > policy.max_entries {
return Err(RetentionError::PackEntryLimit);
}
let file = file.map_err(RetentionError::read)?;
let path = file.path();
let metadata = fs::symlink_metadata(&path).map_err(RetentionError::read)?;
if !metadata.is_file() {
return Err(RetentionError::NonFilePackEntry(path.clone()));
}
if path.extension().is_some_and(|ext| ext == "keep") {
plan.protected_packs.insert(path.with_extension("pack"));
} else if path.extension().is_some_and(|ext| ext == "pack") {
let modified = metadata.modified().map_err(RetentionError::read)?;
if policy.recent_cutoff.is_none_or(|cutoff| modified >= cutoff) {
plan.protected_packs.insert(path);
}
}
}
continue;
}
let Some(prefix) = name.to_str() else {
return Err(RetentionError::NonUtf8ObjectFanout);
};
if prefix == "info" {
continue;
}
if prefix.len() != 2 || !prefix.bytes().all(|b| b.is_ascii_hexdigit()) {
return Err(RetentionError::UnknownObjectDirectoryEntry);
}
for file in fs::read_dir(dir.path()).map_err(RetentionError::read)? {
check(cancel)?;
entries += 1;
if entries > policy.max_entries {
return Err(RetentionError::LooseObjectEntryLimit);
}
let file = file.map_err(RetentionError::read)?;
let metadata = fs::symlink_metadata(file.path()).map_err(RetentionError::read)?;
if !metadata.is_file() {
return Err(RetentionError::NonFileLooseObject);
}
let modified = metadata.modified().map_err(RetentionError::read)?;
if policy.recent_cutoff.is_none_or(|cutoff| modified > cutoff) {
let Some(suffix) = file.file_name().to_str().map(str::to_owned) else {
return Err(RetentionError::NonUtf8LooseObject);
};
let id = ObjectId::from_hex(repo.object_format(), &format!("{prefix}{suffix}"))
.map_err(RetentionError::malformed)?;
plan.roots.insert(id);
plan.strong_roots.insert(id);
}
}
}
Ok(())
}
fn walk(
repo: &Repository,
store: &crate::Objects,
policy: &RetentionPolicy,
cancel: &AtomicBool,
roots: &BTreeSet<ObjectId>,
object_info: &mut BTreeMap<ObjectId, ObjectInfo>,
) -> Result<BTreeSet<ObjectId>, RetentionError> {
let mut pending: VecDeque<_> = roots.iter().copied().collect();
let mut seen = IdSet::default();
let mut expected = IdMap::default();
let mut kinds = IdMap::default();
let mut bytes = 0u64;
let mut edges = 0usize;
let mut blobs = Vec::new();
while let Some(id) = pending.pop_front() {
check(cancel)?;
if seen.contains(&id) {
continue;
}
if seen.len() >= policy.max_entries {
return Err(RetentionError::ReachableObjectLimit);
}
if expected.get(&id) == Some(&ObjectKind::Blob) && !roots.contains(&id) {
seen.insert(id);
blobs.push(id);
continue;
}
let object = store
.read_controlled(id, policy.read, cancel)
.map_err(RetentionError::read)?
.ok_or_else(|| RetentionError::MissingObject(id))?;
if expected
.get(&id)
.is_some_and(|asserted| *asserted != object.kind())
{
return Err(RetentionError::WrongObjectKind(id));
}
if object.kind() == ObjectKind::Blob {
let kind = ObjectKind::Blob;
let size = object.data().len() as u64;
kinds.insert(id, kind);
object_info.insert(id, ObjectInfo { kind, size });
seen.insert(id);
continue;
}
bytes = bytes
.checked_add(object.data().len() as u64)
.ok_or(RetentionError::ReachableByteOverflow)?;
if bytes > policy.max_bytes {
return Err(RetentionError::ReachableByteLimit);
}
let mut add = |target: ObjectId, kind: ObjectKind| -> Result<(), RetentionError> {
edges += 1;
if edges > policy.max_edges {
return Err(RetentionError::ReachableEdgeLimit);
}
if target.is_null() {
return Err(RetentionError::NullEdge(id));
}
if kinds.get(&target).is_some_and(|actual| *actual != kind) {
return Err(RetentionError::WrongObjectKind(target));
}
if let Some(previous) = expected.insert(target, kind)
&& previous != kind
{
return Err(RetentionError::ConflictingObjectKind(target));
}
pending.push_back(target);
Ok(())
};
match object.kind() {
ObjectKind::Blob => {}
ObjectKind::Commit => {
let commit = Commit::parse(repo.object_format(), object.data())
.map_err(RetentionError::malformed)?;
add(commit.tree(), ObjectKind::Tree)?;
if !repo.shallow_roots().contains(id) {
for &parent in commit.parents() {
add(parent, ObjectKind::Commit)?;
}
}
}
ObjectKind::Tree => {
let tree = Tree::parse(repo.object_format(), object.data())
.map_err(RetentionError::malformed)?;
for entry in tree.entries() {
match entry.mode {
crate::EntryMode::Gitlink => {}
crate::EntryMode::Tree => add(entry.id, ObjectKind::Tree)?,
_ => add(entry.id, ObjectKind::Blob)?,
}
}
}
ObjectKind::Tag => {
let tag = Tag::parse(repo.object_format(), object.data())
.map_err(RetentionError::malformed)?;
add(tag.target(), tag.target_kind())?;
}
}
kinds.insert(id, object.kind());
object_info.insert(
id,
ObjectInfo {
kind: object.kind(),
size: object.data().len() as u64,
},
);
seen.insert(id);
}
let located = store
.locate_all(blobs, policy.read, cancel)
.map_err(RetentionError::read)?;
for (id, located) in located {
let (kind, size) = located.ok_or_else(|| RetentionError::MissingObject(id))?;
if kind != ObjectKind::Blob {
return Err(RetentionError::WrongObjectKind(id));
}
object_info.insert(id, ObjectInfo { kind, size });
}
let reachable: BTreeSet<_> = seen.into_iter().collect();
Ok(reachable)
}
fn canonical_empty_tree(id: ObjectId) -> Option<Object> {
let empty = Object {
kind: ObjectKind::Tree,
format: id.format(),
data: Default::default(),
};
(id == empty.id()).then_some(empty)
}
fn check(cancel: &AtomicBool) -> Result<(), RetentionError> {
if cancel.load(Ordering::Relaxed) {
Err(RetentionError::Cancelled)
} else {
Ok(())
}
}