use std::collections::BTreeSet;
use std::io::{self, Write};
use std::path::PathBuf;
use std::process::ExitCode;
use std::time::{Duration, SystemTime};
use color_eyre::eyre::{Result, WrapErr as _, bail, eyre};
use girt::pack::{IndexRebuildLimits, IndexRebuildOutcome, rebuild_index};
use girt::retention::{
MaintenanceIsolation, ReflogExpiryPolicy, RepackLimits, RetentionPlan, RetentionPolicy,
};
use girt::{ObjectFormat, ObjectId, ObjectStore, Objects, Repository};
use crate::Context;
use crate::common::{not_cancelled, objects, stdout};
use crate::object::loose_ids;
const PRUNE_AGE: Duration = Duration::from_secs(14 * 24 * 60 * 60);
const REFLOG_LIVE_AGE: Duration = Duration::from_secs(90 * 24 * 60 * 60);
const REFLOG_DISCONNECTED_AGE: Duration = Duration::from_secs(30 * 24 * 60 * 60);
#[derive(Debug, clap::Subcommand)]
pub enum Command {
Plan,
Repack,
Run(RunArgs),
}
#[derive(Debug, clap::Args)]
pub struct RunArgs {
#[arg(long)]
prune_loose: bool,
#[arg(long)]
expire_reflogs: bool,
#[arg(long)]
retire_packs: bool,
#[arg(long)]
yes: bool,
}
#[derive(Debug, clap::Subcommand)]
pub enum PackCommand {
List,
Verify,
Reindex {
pack: PathBuf,
},
}
pub fn run(command: Command, context: &Context) -> Result<ExitCode> {
let repository = context.repository()?;
match command {
Command::Plan => {
let plan = plan(&repository, &RetentionPolicy::trusted())?;
print_plan(&repository, &plan)?;
}
Command::Repack => {
let progress =
crate::progress::Activity::new(context.progress, "Repacking retained objects");
let published = repository
.repack_retained(
&RetentionPolicy::trusted(),
RepackLimits::trusted(),
not_cancelled(),
)
.wrap_err("repacking retained objects")?;
let written = published.written;
progress.finish(format_args!(
"{} objects, {} bytes",
written.objects, written.pack_bytes
));
let path = repository
.object_dir()
.join("pack")
.join(format!("pack-{}.pack", written.checksum));
writeln!(
stdout(),
"{} {} {}",
path.display(),
written.objects,
written.pack_bytes
)?;
}
Command::Run(args) => return run_phases(&repository, &args, context.progress),
}
Ok(ExitCode::SUCCESS)
}
pub fn pack(command: PackCommand, context: &Context) -> Result<ExitCode> {
match command {
PackCommand::List => {
let repository = context.repository()?;
let objects = objects(&repository)?;
let mut out = stdout();
for store in objects.stores() {
for pack in store.packs() {
write!(
out,
"{} {} {}",
pack.path().display(),
pack.object_count(),
pack.pack_bytes()
)?;
let marks = [
(pack.is_kept(), "keep"),
(pack.is_promisor(), "promisor"),
(store.is_alternate(), "alternate"),
];
for (_, mark) in marks.iter().filter(|(set, _)| *set) {
write!(out, " {mark}")?;
}
writeln!(out)?;
}
}
}
PackCommand::Verify => {
let repository = context.repository()?;
let progress = crate::progress::Activity::new(context.progress, "Verifying packs");
match objects(&repository)?.verify_packs(not_cancelled()) {
Ok(()) => {
progress.finish("verified");
writeln!(stdout(), "ok")?;
}
Err(error) => {
progress.clear();
eprintln!("error: {:#}", color_eyre::eyre::Report::new(error));
return Ok(ExitCode::FAILURE);
}
}
}
PackCommand::Reindex { pack } => {
let format = context
.repository()
.map_or(ObjectFormat::Sha1, |repository| repository.object_format());
let progress =
crate::progress::Activity::new(context.progress, "Rebuilding pack index");
let rebuilt = rebuild_index(
&pack,
format,
IndexRebuildLimits::default(),
not_cancelled(),
)
.wrap_err_with(|| format!("rebuilding index for {}", pack.display()))?;
progress.finish(format_args!("{} objects", rebuilt.objects));
let mut out = stdout();
if context.git {
writeln!(out, "{}", rebuilt.checksum)?;
} else {
let outcome = match rebuilt.outcome {
IndexRebuildOutcome::Created => "created",
IndexRebuildOutcome::Replaced => "replaced",
IndexRebuildOutcome::Unchanged => "unchanged",
_ => "rebuilt",
};
writeln!(out, "{outcome} {} {}", rebuilt.checksum, rebuilt.objects)?;
}
}
}
Ok(ExitCode::SUCCESS)
}
struct UserAsserted;
impl MaintenanceIsolation for UserAsserted {
type Guard = ();
fn acquire(&mut self, _: &Repository) -> io::Result<()> {
Ok(())
}
}
fn run_phases(repository: &Repository, args: &RunArgs, progress_enabled: bool) -> Result<ExitCode> {
if !(args.prune_loose || args.expire_reflogs || args.retire_packs) {
bail!("select at least one of --prune-loose, --expire-reflogs, --retire-packs");
}
let now = SystemTime::now();
let mut policy = RetentionPolicy::trusted();
if args.prune_loose || args.retire_packs {
policy = policy.with_recent_cutoff(now - PRUNE_AGE);
}
if args.expire_reflogs {
let before = |age: Duration| {
let cutoff = now - age;
cutoff
.duration_since(SystemTime::UNIX_EPOCH)
.map_or(0, |since| i128::from(since.as_secs()))
};
policy = policy.with_reflog_expiry(ReflogExpiryPolicy::ClassifiedCutoffs {
live_before: Some(before(REFLOG_LIVE_AGE)),
disconnected_before: Some(before(REFLOG_DISCONNECTED_AGE)),
});
}
let mut out = stdout();
if !args.yes {
let plan = plan(repository, &policy)?;
print_plan(repository, &plan)?;
let (loose, _) = unreachable(repository, &plan)?;
if args.expire_reflogs {
let records: usize = plan.reflog_expiry_candidates.values().map(Vec::len).sum();
writeln!(out, "would-expire-reflog-records {records}")?;
}
if args.retire_packs {
let objects = objects(repository)?;
let retired = primary(&objects)?
.packs()
.filter(|pack| !plan.protected_packs.contains(pack.path()))
.count();
writeln!(out, "would-retire-packs {retired}")?;
}
if args.prune_loose {
writeln!(out, "would-prune-loose {}", loose.len())?;
}
writeln!(
out,
"dry run: pass --yes to run (nothing else may use the repository)"
)?;
return Ok(ExitCode::SUCCESS);
}
let cancel = not_cancelled();
let mut isolation = UserAsserted;
if args.expire_reflogs {
let progress = crate::progress::Activity::new(progress_enabled, "Expiring reflogs");
let report = repository
.expire_reflogs(&mut isolation, &policy, cancel)
.wrap_err("expiring reflogs")?;
progress.finish(format_args!("{} reflogs", report.changed.len()));
writeln!(out, "expired-reflogs {}", report.changed.len())?;
}
if args.retire_packs {
let progress = crate::progress::Activity::new(progress_enabled, "Retiring packs");
let report = repository
.retire_old_packs(&mut isolation, &policy, RepackLimits::trusted(), cancel)
.wrap_err("retiring old packs")?;
progress.finish(format_args!("{} packs removed", report.removed.len()));
if let Some(published) = report.published {
writeln!(out, "new-pack pack-{}", published.written.checksum)?;
}
for path in &report.removed {
writeln!(out, "removed {}", path.display())?;
}
}
if args.prune_loose {
let progress = crate::progress::Activity::new(progress_enabled, "Pruning loose objects");
let report = repository
.prune_unreachable_loose(&mut isolation, &policy, cancel)
.wrap_err("pruning loose objects")?;
progress.finish(format_args!("{} objects", report.deleted.len()));
writeln!(out, "pruned-loose {}", report.deleted.len())?;
}
Ok(ExitCode::SUCCESS)
}
fn plan(repository: &Repository, policy: &RetentionPolicy) -> Result<RetentionPlan> {
let plan = repository.plan_retention(policy, not_cancelled());
if let girt::retention::RetentionOutcome::Incomplete(error) = &plan.outcome {
bail!("retention scan incomplete: {error}");
}
Ok(plan)
}
fn print_plan(repository: &Repository, plan: &RetentionPlan) -> Result<()> {
let (loose, packed) = unreachable(repository, plan)?;
let mut out = stdout();
writeln!(out, "roots {}", plan.roots.len())?;
writeln!(out, "retained {}", plan.required.len())?;
writeln!(out, "reachable {}", plan.reachable.len())?;
writeln!(out, "unreachable-loose {}", loose.len())?;
writeln!(out, "unreachable-packed {}", packed.len())?;
writeln!(out, "protected-packs {}", plan.protected_packs.len())?;
writeln!(out, "alternates {}", plan.alternate_stores.len())?;
Ok(())
}
fn unreachable(
repository: &Repository,
plan: &RetentionPlan,
) -> Result<(BTreeSet<ObjectId>, BTreeSet<ObjectId>)> {
let objects = objects(repository)?;
let store = primary(&objects)?;
let keep = |id: &ObjectId| !plan.reachable.contains(id) && !plan.required.contains(id);
let mut loose: BTreeSet<_> = loose_ids(&store)?.into_iter().collect();
loose.retain(keep);
let mut packed: BTreeSet<_> = store.packs().flat_map(|pack| pack.ids()).collect();
packed.retain(keep);
Ok((loose, packed))
}
fn primary(objects: &Objects) -> Result<ObjectStore<'_>> {
objects
.stores()
.next()
.ok_or_else(|| eyre!("object storage has no primary store"))
}