use std::collections::HashSet;
use std::path::PathBuf;
use std::sync::atomic::{AtomicBool, Ordering};
use super::{AutoThreshold, MaintenanceConfig, MaintenanceTask, TaskSelection};
use crate::commit_graph::{CommitGraph, GraphInstallReport, GraphWriteMode};
use crate::pack::LoosePackLimits;
use crate::refs::{PackRefsLimits, PackRefsOptions, PackRefsReport, Target};
use crate::retention::{MaintenanceIsolation, PrunePackedLimits, PrunePackedSelection};
use crate::{AlternateLimits, HistoryLimits, ObjectKind, PackLimits, PeelLimits, Repository};
mod incremental;
mod loose;
pub use incremental::{IncrementalRepackLimits, IncrementalRepackReport};
pub use loose::LooseObjectsReport;
#[derive(Clone, Copy, Debug)]
#[non_exhaustive]
pub struct MaintenanceLimits {
pub packs: PackLimits,
pub alternates: AlternateLimits,
pub pack_refs: PackRefsLimits,
pub history: HistoryLimits,
pub reference_entries: usize,
pub reference_bytes: usize,
pub peel: PeelLimits,
pub shallow_bytes: usize,
pub prune_packed: PrunePackedLimits,
pub loose_pack: LoosePackLimits,
pub incremental_repack: IncrementalRepackLimits,
}
impl Default for MaintenanceLimits {
fn default() -> Self {
let packs = PackLimits::default();
let mut prune_packed =
PrunePackedLimits::default().with_selection(PrunePackedSelection::Prefix {
max_objects: 50_000,
});
prune_packed.packs = packs;
Self {
packs,
alternates: AlternateLimits::default(),
pack_refs: PackRefsLimits::default(),
history: HistoryLimits::default()
.with_max_commits(5_000_000)
.with_max_parents(20_000_000),
reference_entries: 100_000,
reference_bytes: 16 * 1024 * 1024,
peel: PeelLimits::default(),
shallow_bytes: 16 * 1024 * 1024,
prune_packed,
loose_pack: LoosePackLimits::default(),
incremental_repack: IncrementalRepackLimits::default(),
}
}
}
impl MaintenanceLimits {
#[must_use]
pub fn with_incremental_repack(mut self, value: IncrementalRepackLimits) -> Self {
self.incremental_repack = value;
self
}
#[must_use]
pub fn with_packs(mut self, value: PackLimits) -> Self {
self.packs = value;
self
}
#[must_use]
pub fn with_alternates(mut self, value: AlternateLimits) -> Self {
self.alternates = value;
self
}
#[must_use]
pub fn with_pack_refs(mut self, value: PackRefsLimits) -> Self {
self.pack_refs = value;
self
}
#[must_use]
pub fn with_history(mut self, value: HistoryLimits) -> Self {
self.history = value;
self
}
#[must_use]
pub fn with_reference_entries(mut self, value: usize) -> Self {
self.reference_entries = value;
self
}
#[must_use]
pub fn with_reference_bytes(mut self, value: usize) -> Self {
self.reference_bytes = value;
self
}
#[must_use]
pub fn with_peel(mut self, value: PeelLimits) -> Self {
self.peel = value;
self
}
#[must_use]
pub fn with_shallow_bytes(mut self, value: usize) -> Self {
self.shallow_bytes = value;
self
}
#[must_use]
pub fn with_prune_packed(mut self, value: PrunePackedLimits) -> Self {
self.prune_packed = value;
self
}
#[must_use]
pub fn with_loose_pack(mut self, value: LoosePackLimits) -> Self {
self.loose_pack = value;
self
}
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
#[non_exhaustive]
pub enum TaskSkip {
NotDue,
CommitGraphDisabled,
Shallow,
NoCommits,
}
#[derive(Debug, Default)]
#[non_exhaustive]
pub enum TaskOutcome {
#[default]
NotStarted,
Skipped(TaskSkip),
PackRefs(PackRefsReport),
CommitGraph(GraphInstallReport),
LooseObjects(Box<LooseObjectsReport>),
IncrementalRepack(Box<IncrementalRepackReport>),
}
#[derive(Debug)]
#[non_exhaustive]
pub struct TaskReport {
pub task: MaintenanceTask,
pub outcome: TaskOutcome,
}
#[derive(Debug, Default)]
#[non_exhaustive]
pub struct MaintenanceReport {
pub tasks: Vec<TaskReport>,
}
#[derive(Debug, thiserror::Error)]
#[error("maintenance failed: {source}")]
#[non_exhaustive]
pub struct MaintenanceFailure {
pub report: Box<MaintenanceReport>,
pub failed_task: Option<MaintenanceTask>,
pub cleanup_error: Option<Box<super::LockError>>,
#[source]
pub source: MaintenanceRunError,
}
#[derive(Debug, thiserror::Error)]
#[non_exhaustive]
pub enum MaintenanceRunError {
#[error("maintenance task '{0}' cannot be executed yet")]
Unsupported(MaintenanceTask),
#[error("maintenance requires explicit task selection")]
Selection,
#[error("maintenance cancelled")]
Cancelled,
#[error("maintenance limit exceeded: {0}")]
Limit(&'static str),
#[error("cannot acquire maintenance isolation")]
Isolation(#[source] std::io::Error),
#[error("maintenance primitive failed")]
Primitive(#[source] Box<crate::Error>),
#[error("cannot inspect maintenance input {path}")]
InputIo {
path: PathBuf,
#[source]
source: std::io::Error,
},
#[error("maintenance refuses non-regular loose storage: {0}")]
NonRegularLoose(PathBuf),
}
impl MaintenanceRunError {
fn primitive(error: impl Into<crate::Error>) -> Self {
Self::Primitive(Box::new(error.into()))
}
}
#[derive(Debug)]
pub struct MaintenanceRun<'a> {
repo: &'a Repository,
selection: TaskSelection,
limits: MaintenanceLimits,
auto: bool,
}
impl<'a> MaintenanceRun<'a> {
pub fn new(repo: &'a Repository, selection: TaskSelection) -> Self {
Self {
repo,
selection,
limits: MaintenanceLimits::default(),
auto: false,
}
}
#[must_use]
pub fn with_limits(mut self, limits: MaintenanceLimits) -> Self {
self.limits = limits;
self
}
#[must_use]
pub fn with_auto(mut self, auto: bool) -> Self {
self.auto = auto;
self
}
pub fn run<I: MaintenanceIsolation>(
self,
isolation: &mut I,
cancel: &AtomicBool,
) -> Result<MaintenanceReport, MaintenanceFailure> {
let mut report = MaintenanceReport {
tasks: self
.selection
.tasks()
.iter()
.map(|&task| TaskReport {
task,
outcome: TaskOutcome::default(),
})
.collect(),
};
let mut failed_task = None;
let mut cleanup_error = None;
let mut operation = || -> Result<(), MaintenanceRunError> {
if self.selection.tasks().is_empty() {
return Err(MaintenanceRunError::Selection);
}
for &task in self.selection.tasks() {
if (task == MaintenanceTask::IncrementalRepack && !cfg!(unix))
|| !matches!(
task,
MaintenanceTask::PackRefs
| MaintenanceTask::CommitGraph
| MaintenanceTask::LooseObjects
| MaintenanceTask::IncrementalRepack
)
{
return Err(MaintenanceRunError::Unsupported(task));
}
}
checkpoint(cancel)?;
let _isolation = isolation
.acquire(self.repo)
.map_err(MaintenanceRunError::Isolation)?;
let lock = self
.repo
.try_lock_maintenance()
.map_err(MaintenanceRunError::primitive)?;
let result = (|| {
for task in &mut report.tasks {
checkpoint(cancel)?;
failed_task = Some(task.task);
if task.task == MaintenanceTask::LooseObjects {
loose::run(&self, &_isolation, &mut task.outcome, cancel)?;
} else if task.task == MaintenanceTask::IncrementalRepack {
incremental::run(&self, &_isolation, &mut task.outcome, cancel)?;
} else {
task.outcome = self.task(task.task, cancel)?;
}
failed_task = None;
}
Ok(())
})();
match (result, lock.release()) {
(Err(source), Err(cleanup)) => {
cleanup_error = Some(Box::new(cleanup));
Err(source)
}
(Err(source), Ok(())) => Err(source),
(Ok(()), Err(cleanup)) => Err(MaintenanceRunError::primitive(cleanup)),
(Ok(()), Ok(())) => Ok(()),
}
};
match operation() {
Ok(()) => Ok(report),
Err(source) => Err(MaintenanceFailure {
report: Box::new(report),
failed_task,
cleanup_error,
source,
}),
}
}
fn task(
&self,
task: MaintenanceTask,
cancel: &AtomicBool,
) -> Result<TaskOutcome, MaintenanceRunError> {
if task == MaintenanceTask::CommitGraph {
return self.graph(cancel);
}
let objects = self
.repo
.objects_controlled(self.limits.packs, self.limits.alternates, cancel)
.map_err(MaintenanceRunError::primitive)?;
let refs = self
.repo
.references()
.map_err(MaintenanceRunError::primitive)?;
let options = if self.auto {
PackRefsOptions::AUTO
} else {
PackRefsOptions::ALL
};
refs.pack_controlled(&objects, options, self.limits.pack_refs, cancel)
.map(TaskOutcome::PackRefs)
.map_err(MaintenanceRunError::primitive)
}
fn graph(&self, cancel: &AtomicBool) -> Result<TaskOutcome, MaintenanceRunError> {
if self
.repo
.config()
.boolean("core", None, "commitGraph")
.map_err(MaintenanceRunError::primitive)?
== Some(false)
{
return Ok(TaskOutcome::Skipped(TaskSkip::CommitGraphDisabled));
}
let shallow = crate::ShallowRoots::read(
self.repo.common_dir().join("shallow"),
self.repo.object_format(),
self.limits.shallow_bytes,
cancel,
)
.map_err(MaintenanceRunError::primitive)?;
if !shallow.is_empty() {
return Ok(TaskOutcome::Skipped(TaskSkip::Shallow));
}
let threshold = if self.auto {
MaintenanceConfig::new(self.repo.config())
.auto_threshold(MaintenanceTask::CommitGraph)
.map_err(MaintenanceRunError::primitive)?
} else {
None
};
if threshold == Some(AutoThreshold::Disabled) {
return Ok(TaskOutcome::Skipped(TaskSkip::NotDue));
}
let refs = self
.repo
.references()
.map_err(MaintenanceRunError::primitive)?;
let listed = refs
.list_controlled(
self.limits.reference_entries,
self.limits.reference_bytes,
cancel,
)
.map_err(MaintenanceRunError::primitive)?;
let objects = self
.repo
.objects_controlled(self.limits.packs, self.limits.alternates, cancel)
.map_err(MaintenanceRunError::primitive)?;
let mut roots = Vec::new();
for reference in &listed {
checkpoint(cancel)?;
let mut target = &reference.target;
let mut depth = 0;
while let Target::Symbolic(name) = target {
depth += 1;
if depth > 32 {
return Err(MaintenanceRunError::Limit("symbolic reference depth"));
}
let Ok(position) = listed.binary_search_by(|entry| entry.name.cmp(name)) else {
break;
};
target = &listed[position].target;
}
if let Target::Direct(id) = target {
let peeled = objects
.peel(*id, self.limits.peel, cancel)
.map_err(MaintenanceRunError::primitive)?;
if peeled.kind == ObjectKind::Commit {
roots.push(peeled.target);
}
}
}
roots.sort_unstable();
roots.dedup();
if roots.is_empty() {
return Ok(TaskOutcome::Skipped(TaskSkip::NoCommits));
}
if let Some(AutoThreshold::Count(count)) = threshold {
let old = self
.repo
.commit_graph()
.map_err(MaintenanceRunError::primitive)?;
let mut pending = roots.clone();
let mut seen = HashSet::new();
let mut parents = 0usize;
while let Some(id) = pending.pop() {
checkpoint(cancel)?;
if old.as_ref().is_some_and(|graph| graph.get(id).is_some()) || seen.contains(&id) {
continue;
}
if seen.len() == self.limits.history.max_commits {
return Err(MaintenanceRunError::Limit("commits"));
}
seen.insert(id);
if seen.len() >= count as usize {
break;
}
let commit = objects
.read_commit_with(id, self.limits.history.read)
.map_err(MaintenanceRunError::primitive)?;
parents = parents
.checked_add(commit.parents().len())
.ok_or(MaintenanceRunError::Limit("parents"))?;
if parents > self.limits.history.max_parents {
return Err(MaintenanceRunError::Limit("parents"));
}
pending.extend_from_slice(commit.parents());
}
if seen.len() < count as usize {
return Ok(TaskOutcome::Skipped(TaskSkip::NotDue));
}
}
checkpoint(cancel)?;
let graph =
CommitGraph::from_objects_controlled(&objects, &roots, self.limits.history, cancel)
.map_err(MaintenanceRunError::primitive)?;
checkpoint(cancel)?;
graph
.install(self.repo.object_dir(), GraphWriteMode::Incremental)
.map(TaskOutcome::CommitGraph)
.map_err(MaintenanceRunError::primitive)
}
}
fn checkpoint(cancel: &AtomicBool) -> Result<(), MaintenanceRunError> {
if cancel.load(Ordering::Relaxed) {
Err(MaintenanceRunError::Cancelled)
} else {
Ok(())
}
}
struct BorrowedIsolation<'a, G> {
_guard: &'a G,
}
impl<G> MaintenanceIsolation for BorrowedIsolation<'_, G> {
type Guard = ();
fn acquire(&mut self, _: &Repository) -> std::io::Result<()> {
Ok(())
}
}