use super::Error;
use crate::config::{CompactionConfig, CompactionTrigger};
use crate::template::GitRunner;
use std::path::Path;
pub(super) const MERGE_SUBJECT_PREFIX: &str = "compaction merge [";
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct CheckpointState {
pub commits_since_checkpoint: u32,
pub seconds_since_checkpoint: u64,
pub flush_requested: bool,
}
pub fn due(cfg: Option<&CompactionConfig>, state: &CheckpointState) -> bool {
let Some(cfg) = cfg else {
return false;
};
let threshold = |v: u64| {
cfg.intermediate
.n
.is_some_and(|n| n > 0 && v >= u64::from(n))
};
match cfg.intermediate.trigger {
CompactionTrigger::EveryNCommits => threshold(u64::from(state.commits_since_checkpoint)),
CompactionTrigger::EveryTSeconds => threshold(state.seconds_since_checkpoint),
CompactionTrigger::OnFlush => state.flush_requested,
}
}
pub fn state(
worktree: &Path,
now_unix: u64,
flush_requested: bool,
git: &dyn GitRunner,
) -> Result<CheckpointState, Error> {
let last = last_checkpoint(worktree, git)?;
Ok(CheckpointState {
commits_since_checkpoint: commits_since(worktree, last.as_deref(), git)?,
seconds_since_checkpoint: now_unix.saturating_sub(checkpoint_time(worktree, &last, git)?),
flush_requested,
})
}
fn commits_since(worktree: &Path, last: Option<&str>, git: &dyn GitRunner) -> Result<u32, Error> {
let range = match last {
Some(sha) => format!("{sha}..HEAD"),
None => "HEAD".to_string(),
};
let out = git
.run_capture(worktree, &["rev-list", "--count", &range])
.map_err(|source| Error::Git {
op: "checkpoint rev-list count",
source,
})?;
Ok(out.trim().parse::<u32>().unwrap_or(0))
}
fn checkpoint_time(
worktree: &Path,
last: &Option<String>,
git: &dyn GitRunner,
) -> Result<u64, Error> {
let reference = match last {
Some(sha) => sha.clone(),
None => root_commit(worktree, git)?,
};
let out = git
.run_capture(worktree, &["log", "-n", "1", "--format=%ct", &reference])
.map_err(|source| Error::Git {
op: "checkpoint commit time",
source,
})?;
Ok(out.trim().parse::<u64>().unwrap_or(0))
}
fn root_commit(worktree: &Path, git: &dyn GitRunner) -> Result<String, Error> {
let out = git
.run_capture(worktree, &["rev-list", "--max-parents=0", "HEAD"])
.map_err(|source| Error::Git {
op: "checkpoint root rev-list",
source,
})?;
Ok(out.lines().last().unwrap_or("").trim().to_string())
}
fn last_checkpoint(worktree: &Path, git: &dyn GitRunner) -> Result<Option<String>, Error> {
let out = git
.run_capture(
worktree,
&[
"log",
"-n",
"1",
"--format=%H",
"-F",
"--grep",
MERGE_SUBJECT_PREFIX,
],
)
.map_err(|source| Error::Git {
op: "checkpoint log grep",
source,
})?;
let sha = out.trim();
Ok((!sha.is_empty()).then(|| sha.to_string()))
}
#[cfg(test)]
mod tests;