use crate::policy::PolicyFile;
use crate::store::{Store, StoreError};
use lex_vcs::{OpId, OpLog, Predicate};
use serde::{Deserialize, Serialize};
use std::collections::{BTreeMap, BTreeSet};
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum RetentionReason {
ReachableFromBranch,
MatchedPredicate(usize),
ParentOfRetained,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct GcPlan {
pub retained: BTreeMap<OpId, RetentionReason>,
pub to_delete: Vec<OpId>,
}
impl GcPlan {
pub fn is_empty(&self) -> bool {
self.to_delete.is_empty()
}
}
impl Store {
pub fn plan_gc(
&self,
cli_retain: &[Predicate],
) -> Result<GcPlan, StoreError> {
let log = OpLog::open(self.root())?;
let universe: BTreeSet<OpId> = log
.list_all()?
.into_iter()
.map(|r| r.op_id)
.collect();
let mut retained: BTreeMap<OpId, RetentionReason> = BTreeMap::new();
for branch_name in self.list_branches()? {
let Some(branch) = self.get_branch(&branch_name)? else { continue };
let Some(head) = branch.head_op else { continue };
for rec in log.walk_back(&head, None)? {
retained
.entry(rec.op_id)
.or_insert(RetentionReason::ReachableFromBranch);
}
}
let mut all_retain: Vec<Predicate> = cli_retain.to_vec();
let policy = PolicyFile::load_optional(self.root())?;
for (i, raw) in policy.gc_retention.retain.iter().enumerate() {
let pred = Predicate::from_value(raw)
.map_err(|e| StoreError::Io(std::io::Error::new(
std::io::ErrorKind::InvalidData,
format!("policy.gc_retention.retain[{i}]: {e}"),
)))?;
all_retain.push(pred);
}
for (i, predicate) in all_retain.iter().enumerate() {
for rec in lex_vcs::evaluate(&log, predicate)? {
retained
.entry(rec.op_id)
.or_insert(RetentionReason::MatchedPredicate(i));
}
}
let frontier: Vec<OpId> = retained.keys().cloned().collect();
for op_id in frontier {
for rec in log.walk_back(&op_id, None)? {
retained
.entry(rec.op_id)
.or_insert(RetentionReason::ParentOfRetained);
}
}
let to_delete: Vec<OpId> = universe
.iter()
.filter(|id| !retained.contains_key(*id))
.cloned()
.collect();
Ok(GcPlan { retained, to_delete })
}
pub fn apply_gc(&self, plan: &GcPlan) -> Result<usize, StoreError> {
if plan.to_delete.is_empty() {
return Ok(0);
}
let log = OpLog::open(self.root())?;
let victims: BTreeSet<OpId> = plan.to_delete.iter().cloned().collect();
Ok(log.evict(&victims)?)
}
}
impl PolicyFile {
fn load_optional(root: &std::path::Path) -> std::io::Result<Self> {
Ok(crate::policy::load(root)?.unwrap_or_default())
}
}
#[derive(Debug, Clone)]
pub struct BlobGcPlan {
pub live: BTreeSet<crate::files::BlobId>,
pub to_delete: Vec<crate::files::BlobId>,
pub skipped_within_grace: Vec<crate::files::BlobId>,
}
impl BlobGcPlan {
pub fn is_empty(&self) -> bool {
self.to_delete.is_empty()
}
}
impl Store {
pub fn plan_blob_gc(&self, grace: std::time::Duration) -> Result<BlobGcPlan, StoreError> {
let op_plan = self.plan_gc(&[])?;
let log = OpLog::open(self.root())?;
let mut live: BTreeSet<String> = BTreeSet::new();
for op_id in op_plan.retained.keys() {
let Some(rec) = log.get(op_id)? else { continue };
if let lex_vcs::OperationKind::SetFiles { manifest } = &rec.op.kind {
live.insert(manifest.clone());
if let Ok(m) = self.get_manifest(manifest) {
for e in m.entries.values() {
live.insert(e.blob.clone());
}
}
}
}
for sha in self.all_blob_ref_shas()? {
live.insert(sha);
}
let now = std::time::SystemTime::now();
let mut to_delete = Vec::new();
let mut skipped_within_grace = Vec::new();
for (id, mtime) in self.list_blob_ids_with_mtime()? {
if live.contains(&id) {
continue;
}
match now.duration_since(mtime) {
Ok(age) if age >= grace => to_delete.push(id),
_ => skipped_within_grace.push(id),
}
}
to_delete.sort();
skipped_within_grace.sort();
Ok(BlobGcPlan { live, to_delete, skipped_within_grace })
}
pub fn apply_blob_gc(&self, plan: &BlobGcPlan) -> Result<usize, StoreError> {
let mut removed = 0usize;
for id in &plan.to_delete {
self.delete_blob(id)?;
removed += 1;
}
Ok(removed)
}
}