use std::collections::hash_map::Entry;
use std::collections::{BTreeMap, BTreeSet};
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,
index,
};
mod error;
pub use error::{RetentionError, RetentionErrorKind};
mod repack;
pub use repack::{BitmapOutcome, RepackError, RepackLimits, RepackPublished};
mod prune;
mod prune_packed;
pub use prune::{MaintenanceIsolation, PruneCause, PruneFailure, PruneReport};
pub use prune_packed::{
PrunePackedCause, PrunePackedFailure, PrunePackedLimits, PrunePackedReport,
PrunePackedSelection,
};
mod config;
pub(crate) use config::parse_expiry_date;
pub use config::{ReflogCutoffs, ReflogExpiry, ReflogExpiryError};
mod expire;
pub use expire::{ExpireCause, ExpireFailure, ExpireReport};
mod retire;
pub use retire::{RetireCause, RetireFailure, RetireReport};
mod promisor;
#[cfg(all(test, unix))]
mod promisor_tests;
pub use promisor::{IsolatedRepackFailure, IsolatedRepackReport, RetainReason, RetainedPack};
mod compose;
mod consolidate;
pub use consolidate::PackConsolidation;
mod inputs;
mod walk_reads;
#[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 pack_consolidation: PackConsolidation,
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,
pub max_threads: usize,
pub partial_clone: PartialClonePolicy,
pub max_promised_boundaries: usize,
}
#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
#[non_exhaustive]
pub enum PartialClonePolicy {
#[default]
Refuse,
Promised,
}
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_pack_consolidation(mut self, pack_consolidation: PackConsolidation) -> Self {
self.pack_consolidation = pack_consolidation;
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
}
#[must_use]
pub fn with_max_threads(mut self, max_threads: usize) -> Self {
self.max_threads = max_threads;
self
}
#[must_use]
pub fn with_max_promised_boundaries(mut self, max_promised_boundaries: usize) -> Self {
self.max_promised_boundaries = max_promised_boundaries;
self
}
#[must_use]
pub fn with_partial_clone(mut self, partial_clone: PartialClonePolicy) -> Self {
self.partial_clone = partial_clone;
self
}
}
impl Default for RetentionPolicy {
fn default() -> Self {
Self {
heads: Vec::new(),
recent_cutoff: None,
reflog_expiry: ReflogExpiryPolicy::KeepAll,
pack_consolidation: PackConsolidation::Configured,
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(),
max_threads: 8,
partial_clone: PartialClonePolicy::Refuse,
max_promised_boundaries: 1_000_000,
}
}
}
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(),
max_promised_boundaries: usize::MAX / 4,
..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>,
folded_packs: BTreeSet<PathBuf>,
fold_recent_packs: bool,
pub reachable: BTreeSet<ObjectId>,
pub promised_boundaries: BTreeMap<ObjectId, ObjectKind>,
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,
fold_recent_packs: bool,
cancel: &AtomicBool,
) -> RetentionPlan {
self.observe_roots(policy, fold_recent_packs, cancel, true, true, false)
}
fn observe_integrity_roots(
&self,
policy: &RetentionPolicy,
cancel: &AtomicBool,
include_reflogs: bool,
) -> RetentionPlan {
self.observe_roots(policy, false, cancel, false, include_reflogs, true)
}
fn observe_roots(
&self,
policy: &RetentionPolicy,
fold_recent_packs: bool,
cancel: &AtomicBool,
include_recent: bool,
include_reflogs: bool,
allow_partial_clone: bool,
) -> 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(),
folded_packs: BTreeSet::new(),
fold_recent_packs,
reachable: BTreeSet::new(),
promised_boundaries: BTreeMap::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,
include_recent,
include_reflogs,
allow_partial_clone,
) {
plan.fail(reason);
return plan;
}
plan
}
pub(crate) fn integrity_roots(
&self,
policy: &RetentionPolicy,
cancel: &AtomicBool,
include_reflogs: bool,
) -> Result<BTreeSet<ObjectId>, RetentionError> {
let plan = self.observe_integrity_roots(policy, cancel, include_reflogs);
match plan.outcome {
RetentionOutcome::Complete => Ok(plan.roots),
RetentionOutcome::Incomplete(error) => Err(error),
}
}
pub fn plan_retention(&self, policy: &RetentionPolicy, cancel: &AtomicBool) -> RetentionPlan {
self.plan_retention_folding(policy, false, cancel)
}
pub(super) fn plan_retention_folding(
&self,
policy: &RetentionPolicy,
fold_recent_packs: bool,
cancel: &AtomicBool,
) -> RetentionPlan {
let _headers = crate::pack::HeaderScope::new();
let mut plan = self.observe_retention_roots(policy, fold_recent_packs, 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();
let mut boundaries = Boundaries::new(policy, &store);
match walk(
self,
&store,
policy,
cancel,
&roots,
&mut plan.object_info,
&mut boundaries,
) {
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,
&mut boundaries,
) {
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,
&mut boundaries,
) {
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));
let retained_packs: BTreeSet<PathBuf> = plan
.protected_packs
.union(&plan.folded_packs)
.cloned()
.collect();
if (has_candidates || has_old_pack) && !retained_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 !retained_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 known = plan.object_info.get(&id).map(|info| (info.kind, info.size));
let (kind, size) = match known.map_or_else(
|| store.locate(id, policy.read, cancel),
|info| Ok(Some(info)),
) {
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 if !plan.required.contains(&id) {
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,
&mut boundaries,
) {
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.promised_boundaries = boundaries.found;
plan.store = Some(store);
plan
}
}
fn collect_roots(
repository: &Repository,
policy: &RetentionPolicy,
cancel: &AtomicBool,
plan: &mut RetentionPlan,
include_recent: bool,
include_reflogs: bool,
allow_partial_clone: bool,
) -> 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 !allow_partial_clone
&& policy.partial_clone == PartialClonePolicy::Refuse
&& repo.is_partial_clone()
{
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);
}
}
if include_reflogs {
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);
}
}
if include_recent {
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" {
let mut pack_files = BTreeSet::new();
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);
}
}
pack_files.insert(file.path());
}
fold_recent_packs(plan, &pack_files);
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 fold_recent_packs(plan: &mut RetentionPlan, pack_files: &BTreeSet<PathBuf>) {
if !plan.fold_recent_packs {
return;
}
for pack in pack_files.iter().filter(|path| {
path.extension().is_some_and(|ext| ext == "pack")
&& pack_files.contains(&path.with_extension("idx"))
}) {
let foldable = pack_files.iter().all(|path| {
path.file_stem() != pack.file_stem()
|| matches!(
path.extension().and_then(|ext| ext.to_str()),
Some("idx" | "pack" | "rev" | "bitmap")
)
});
if !foldable {
plan.protected_packs.insert(pack.clone());
} else if plan.protected_packs.remove(pack) {
plan.folded_packs.insert(pack.clone());
}
}
}
struct Node {
expected: Option<ObjectKind>,
actual: Option<ObjectKind>,
}
fn walk(
repo: &Repository,
store: &crate::Objects,
policy: &RetentionPolicy,
cancel: &AtomicBool,
roots: &BTreeSet<ObjectId>,
object_info: &mut BTreeMap<ObjectId, ObjectInfo>,
boundaries: &mut Boundaries,
) -> Result<BTreeSet<ObjectId>, RetentionError> {
let visit = |id| visit(repo, store, policy, cancel, id);
let mut admit = |id, kind| boundaries.admit(store, policy, cancel, id, kind);
let (nodes, blobs, mut missing) =
walk_reads::with_reader(policy.max_threads, &visit, |reader| {
walk_with(policy, cancel, roots, object_info, reader, &mut admit)
})?;
let located = store
.locate_all(blobs, policy.read, cancel)
.map_err(RetentionError::read)?;
for (id, located) in located {
let Some((kind, size)) = located else {
admit(id, Some(ObjectKind::Blob))?;
missing.insert(id);
continue;
};
if kind != ObjectKind::Blob {
return Err(RetentionError::WrongObjectKind(id));
}
object_info.insert(id, ObjectInfo { kind, size });
}
Ok(nodes
.into_keys()
.filter(|id| !missing.contains(id))
.collect())
}
struct Boundaries {
enabled: bool,
promised: Option<crate::PromisedObjects>,
found: BTreeMap<ObjectId, ObjectKind>,
}
impl Boundaries {
fn new(policy: &RetentionPolicy, store: &crate::Objects) -> Self {
Self {
enabled: policy.partial_clone == PartialClonePolicy::Promised && store.partial_clone,
promised: None,
found: BTreeMap::new(),
}
}
fn admit(
&mut self,
store: &crate::Objects,
policy: &RetentionPolicy,
cancel: &AtomicBool,
id: ObjectId,
expected: Option<ObjectKind>,
) -> Result<(), RetentionError> {
let Some(kind @ (ObjectKind::Tree | ObjectKind::Blob)) = expected else {
return Err(RetentionError::MissingObject(id));
};
if !self.enabled {
return Err(RetentionError::MissingObject(id));
}
check(cancel)?;
let promised = match &mut self.promised {
Some(promised) => promised,
slot => {
let limits = crate::ListLimits::default().with_max_entries(policy.max_entries);
let promised = store
.promised_objects(limits, policy.read, cancel)
.map_err(|error| match error {
crate::ObjectReadError::Cancelled => RetentionError::Cancelled,
crate::ObjectReadError::Limit(_) => RetentionError::PromisedObjectLimit,
error => RetentionError::read(error),
})?;
slot.insert(promised)
}
};
if !promised.contains(id) {
return Err(RetentionError::MissingObject(id));
}
if !self.found.contains_key(&id) && self.found.len() >= policy.max_promised_boundaries {
return Err(RetentionError::PromisedBoundaryLimit);
}
self.found.insert(id, kind);
Ok(())
}
}
fn walk_with(
policy: &RetentionPolicy,
cancel: &AtomicBool,
roots: &BTreeSet<ObjectId>,
object_info: &mut BTreeMap<ObjectId, ObjectInfo>,
reader: &mut walk_reads::Reader<'_>,
admit: &mut dyn FnMut(ObjectId, Option<ObjectKind>) -> Result<(), RetentionError>,
) -> Result<(IdMap<Node>, Vec<ObjectId>, IdSet), RetentionError> {
let mut nodes = IdMap::default();
for &root in roots {
nodes.insert(
root,
Node {
expected: None,
actual: None,
},
);
reader.push(root, true);
}
let mut visited = 0usize;
let mut bytes = 0u64;
let mut edges = 0usize;
let mut blobs = Vec::new();
let mut missing = IdSet::default();
while let Some((id, read)) = reader.next() {
check(cancel)?;
if visited >= policy.max_entries {
return Err(RetentionError::ReachableObjectLimit);
}
visited += 1;
let Some(read) = read else {
blobs.push(id);
continue;
};
let Visit {
kind,
size,
edges: named,
malformed,
} = match read {
Err(RetentionError::MissingObject(absent)) if absent == id && !roots.contains(&id) => {
admit(id, nodes.get(&id).and_then(|node| node.expected))?;
missing.insert(id);
continue;
}
read => read?,
};
let expected = nodes.get(&id).and_then(|node| node.expected);
if expected.is_some_and(|asserted| asserted != kind) {
return Err(RetentionError::WrongObjectKind(id));
}
if let Some(node) = nodes.get_mut(&id) {
node.actual = Some(kind);
}
object_info.insert(id, ObjectInfo { kind, size });
if kind == ObjectKind::Blob {
continue;
}
bytes = bytes
.checked_add(size)
.ok_or(RetentionError::ReachableByteOverflow)?;
if bytes > policy.max_bytes {
return Err(RetentionError::ReachableByteLimit);
}
for (target, kind) in named {
edges += 1;
if edges > policy.max_edges {
return Err(RetentionError::ReachableEdgeLimit);
}
if target.is_null() {
return Err(RetentionError::NullEdge(id));
}
match nodes.entry(target) {
Entry::Occupied(mut entry) => {
let node = entry.get_mut();
if node.actual.is_some_and(|actual| actual != kind) {
return Err(RetentionError::WrongObjectKind(target));
}
match node.expected {
Some(previous) if previous != kind => {
return Err(RetentionError::ConflictingObjectKind(target));
}
Some(_) => {}
None => node.expected = Some(kind),
}
}
Entry::Vacant(entry) => {
entry.insert(Node {
expected: Some(kind),
actual: None,
});
reader.push(target, kind != ObjectKind::Blob || roots.contains(&target));
}
}
}
if let Some(error) = malformed {
return Err(error);
}
}
Ok((nodes, blobs, missing))
}
struct Visit {
kind: ObjectKind,
size: u64,
edges: Vec<(ObjectId, ObjectKind)>,
malformed: Option<RetentionError>,
}
fn visit(
repo: &Repository,
store: &crate::Objects,
policy: &RetentionPolicy,
cancel: &AtomicBool,
id: ObjectId,
) -> Result<Visit, RetentionError> {
let object = store
.read_controlled(id, policy.read, cancel)
.map_err(RetentionError::read)?
.ok_or_else(|| RetentionError::MissingObject(id))?;
let kind = object.kind();
let size = object.data().len() as u64;
let mut edges = Vec::new();
let mut malformed = None;
let collect = |edges: &Vec<_>| edges.len() <= policy.max_edges;
match kind {
_ if kind != ObjectKind::Blob && size > policy.max_bytes => {}
ObjectKind::Blob => {}
ObjectKind::Commit => match Commit::parse(repo.object_format(), object.data()) {
Ok(commit) => {
edges.push((commit.tree(), ObjectKind::Tree));
if !repo.shallow_roots().contains(id) {
let parents = commit.parents().iter().map(|&p| (p, ObjectKind::Commit));
edges.extend(parents.take(policy.max_edges));
}
}
Err(error) => malformed = Some(RetentionError::malformed(error)),
},
ObjectKind::Tree => {
for entry in crate::tree::RawEntries::new(repo.object_format(), object.data()) {
if !collect(&edges) {
break;
}
match entry {
Ok((crate::EntryMode::Gitlink, _, _)) => {}
Ok((crate::EntryMode::Tree, _, target)) => {
edges.push((target, ObjectKind::Tree));
}
Ok((_, _, target)) => edges.push((target, ObjectKind::Blob)),
Err(error) => {
malformed = Some(RetentionError::malformed(error));
break;
}
}
}
}
ObjectKind::Tag => match Tag::parse(repo.object_format(), object.data()) {
Ok(tag) => edges.push((tag.target(), tag.target_kind())),
Err(error) => malformed = Some(RetentionError::malformed(error)),
},
}
Ok(Visit {
kind,
size,
edges,
malformed,
})
}
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(())
}
}