use std::sync::Arc;
use std::path::{Path, PathBuf};
use std::process::Command;
use anyhow::Context as _;
pub(crate) mod bitmap;
pub(crate) use bitmap::SCRATCH;
#[cfg(test)]
mod tests;
use bitmap::{Before, Bitmap, run_unlocked};
use crate::config::ValidatedConfig;
use crate::generate::Tracer;
use crate::repolock::LockMode;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Action {
Repack,
Gc,
Prune,
}
impl Action {
pub fn parse(raw: &str) -> anyhow::Result<Self> {
match raw {
"repack" => Ok(Self::Repack),
"gc" => Ok(Self::Gc),
"prune" => Ok(Self::Prune),
other => anyhow::bail!(
"unsupported maintenance action {other:?}: expected one of repack, gc, prune"
),
}
}
fn git_args(self) -> &'static [&'static [&'static str]] {
// the geometric repack keeps the biggest pack, and exports hardlink its blocks
const REPACK: &[&str] = &[
"-c",
"core.packedGitLimit=256m",
"-c",
"core.packedGitWindowSize=32m",
"repack",
"-d",
"-q",
"--geometric=2",
];
const GC: &[&str] = &["gc", "--quiet"];
const EXPIRE_REFLOGS: &[&str] = &["reflog", "expire", "--expire=now", "--all"];
const PRUNE: &[&str] = &["prune", "--expire=now"];
match self {
Self::Repack => &[REPACK],
Self::Gc => &[GC],
Self::Prune => &[EXPIRE_REFLOGS, PRUNE],
}
}
/// runs after the lock is released, since it only reads packs and renames its output.
fn unlocked_args(self) -> &'static [&'static [&'static str]] {
const BITMAP: &[&str] = &[
"-c",
"core.packedGitLimit=256m",
"-c",
"core.packedGitWindowSize=32m",
"multi-pack-index",
"write",
"--bitmap",
];
match self {
Self::Repack => &[BITMAP],
Self::Gc | Self::Prune => &[],
}
}
const fn describe(self) -> &'static str {
match self {
Self::Repack => "repack",
Self::Gc => "gc",
Self::Prune => "prune (reflog expire + prune --expire=now)",
}
}
}
pub async fn run_all(config: Arc<ValidatedConfig>, action: Action) -> anyhow::Result<()> {
let mut failed = Vec::new();
for did in crate::registry::hosted(&config)? {
if let Err(error) = run(config.clone(), did.as_str(), action).await {
eprintln!(
"maintenance: {} failed for {did}: {error:#}",
action.describe()
);
failed.push(did.to_string());
}
}
anyhow::ensure!(
failed.is_empty(),
"maintenance failed for {}",
failed.join(", ")
);
Ok(())
}
pub async fn run(
config: Arc<ValidatedConfig>,
repo_did: &str,
action: Action,
) -> anyhow::Result<()> {
let layout = config.layout()?;
let repo_path = layout
.guarded_path(&knot_types::RepoDid::new(repo_did.to_string())?)
.with_context(|| format!("resolve guarded path for {repo_did}"))?;
anyhow::ensure!(
repo_path.exists(),
"hosted repo {repo_did} missing on disk at {}",
repo_path.display()
);
if !maintain(
&config.output_root,
repo_did,
&repo_path,
action,
run_unlocked,
)? {
eprintln!("maintenance: another maintenance of {repo_did} is running; skipped");
}
Ok(())
}
/// the unlocked steps read packs a repack deletes. the maintenance lock keeps one running.
fn maintain(
output_root: &Path,
repo_did: &str,
repo_path: &Path,
action: Action,
unlocked: impl FnOnce(&Path, Option<Before>) -> anyhow::Result<Option<Bitmap>>,
) -> anyhow::Result<bool> {
let dir = crate::repolock::repo_dir(output_root, &crate::plan::repo_key(repo_did));
std::fs::create_dir_all(&dir).with_context(|| format!("create {}", dir.display()))?;
let path = dir.join(MAINTENANCE_LOCK);
let running = std::fs::OpenOptions::new()
.create(true)
.truncate(false)
.write(true)
.open(&path)
.with_context(|| format!("open {}", path.display()))?;
match running.try_lock() {
Ok(()) => {}
Err(std::fs::TryLockError::WouldBlock) => return Ok(false),
Err(std::fs::TryLockError::Error(error)) => {
return Err(error).with_context(|| format!("lock {}", path.display()));
}
}
// read the bitmap before the roll-up, which deletes it with its packs
let trace = Tracer::new();
let before = (action == Action::Repack)
.then(|| trace.time("bitmap: read the last", || Before::read(repo_path)));
let lock = trace.time("maintenance: locked steps", || {
maintain_locked(output_root, repo_did, repo_path, action, run_git)
})?;
eprintln!(
"maintenance: {} completed for {repo_did} (lock {} released)",
action.describe(),
lock.display()
);
if let Some(bitmap) = unlocked(repo_path, before)? {
eprintln!("maintenance: bitmap for {repo_did}: {bitmap}");
}
Ok(true)
}
/// taken before the repo lock, and held for the whole maintenance.
pub(crate) const MAINTENANCE_LOCK: &str = ".maintenance.lock";
fn maintain_locked(
output_root: &Path,
repo_did: &str,
repo_path: &Path,
action: Action,
work: impl FnOnce(&Path, Action) -> anyhow::Result<()>,
) -> anyhow::Result<PathBuf> {
let key = crate::plan::repo_key(repo_did);
eprintln!(
"maintenance: acquiring exclusive lock {} ({})",
crate::repolock::lock_path(output_root, &key).display(),
action.describe()
);
let lock = crate::repolock::acquire(output_root, &key, LockMode::Exclusive)?;
let result = work(repo_path, action);
if result.is_ok() {
crate::commit_graph::rewrite(repo_path);
}
let path = lock.path().to_path_buf();
drop(lock);
result?;
Ok(path)
}
fn run_git(repo_path: &Path, action: Action) -> anyhow::Result<()> {
run_steps(repo_path, action.git_args())
}
pub(crate) fn run_steps(repo_path: &Path, steps: &[&[&str]]) -> anyhow::Result<()> {
for args in steps {
git(repo_path, args, b"")?;
}
Ok(())
}
pub(crate) fn git(repo_path: &Path, args: &[&str], stdin: &[u8]) -> anyhow::Result<Vec<u8>> {
use std::io::Write as _;
let described = format!("git {}", args.join(" "));
let mut child = crate::git_export::true_history(&mut Command::new("git"))
.current_dir(repo_path)
.args(args)
.env("GIT_CONFIG_GLOBAL", "/dev/null")
.env("GIT_CONFIG_SYSTEM", "/dev/null")
.env("GIT_CONFIG_NOSYSTEM", "1")
.env("GIT_CONFIG_COUNT", "2")
.env("GIT_CONFIG_KEY_0", "maintenance.auto")
.env("GIT_CONFIG_VALUE_0", "false")
.env("GIT_CONFIG_KEY_1", "gc.autoDetach")
.env("GIT_CONFIG_VALUE_1", "false")
.env("LC_ALL", "C")
.stdin(std::process::Stdio::piped())
.stdout(std::process::Stdio::piped())
.stderr(std::process::Stdio::piped())
.spawn()
.with_context(|| format!("failed to spawn {described}"))?;
child
.stdin
.take()
.context("git's stdin")?
.write_all(stdin)
.with_context(|| format!("write to {described}"))?;
let output = child
.wait_with_output()
.with_context(|| format!("wait for {described}"))?;
anyhow::ensure!(
output.status.success(),
"{described} failed in {} (status {}): {}{}",
repo_path.display(),
output.status,
String::from_utf8_lossy(&output.stderr),
String::from_utf8_lossy(&output.stdout),
);
Ok(output.stdout)
}