//! Post-push housekeeping for a bare repository: commit-graph refresh and `git maintenance --auto`. use std::path::Path; use std::time::Duration; use crate::command::{GitCommand, GitEnv}; use crate::error::{Error, Result}; /// Upper bound for each git invocation. The child is killed when the timeout fires. const STEP_TIMEOUT: Duration = Duration::from_secs(120); /// Refreshes the commit-graph (with changed-path filters) and runs the auto maintenance tasks /// for the bare repo at `repo`. Each step is bounded by [`STEP_TIMEOUT`]. pub async fn after_push(env: &GitEnv, repo: &Path) -> Result<()> { let graph = GitCommand::new(env, "commit-graph") .git_dir(repo) .arg("write") .arg("--reachable") .arg("--changed-paths"); run_bounded(graph).await?; let maintenance = GitCommand::new(env, "maintenance") .git_dir(repo) .arg("run") .arg("--auto") .arg("--quiet"); run_bounded(maintenance).await } /// Runs `cmd` with [`STEP_TIMEOUT`]. Dropping the future on timeout drops the child, /// which `GitCommand` spawns with `kill_on_drop`, so the process is killed. async fn run_bounded(cmd: GitCommand) -> Result<()> { match tokio::time::timeout(STEP_TIMEOUT, cmd.status()).await { Ok(result) => result, Err(_) => Err(Error::Failed(format!( "timed out after {}s", STEP_TIMEOUT.as_secs() ))), } } #[cfg(test)] mod tests { use super::*; use crate::repo::init_bare; use std::path::PathBuf; fn git(dir: &Path, args: &[&str]) { let out = std::process::Command::new("git") .arg("-C") .arg(dir) .args(args) .env("GIT_AUTHOR_NAME", "test") .env("GIT_AUTHOR_EMAIL", "test@example.com") .env("GIT_COMMITTER_NAME", "test") .env("GIT_COMMITTER_EMAIL", "test@example.com") .output() .expect("git runs"); assert!( out.status.success(), "git {args:?} failed: {}", String::from_utf8_lossy(&out.stderr) ); } fn env_for(tmp: &Path) -> GitEnv { GitEnv { git_bin: PathBuf::from("git"), data_dir: tmp.to_path_buf(), } } #[tokio::test] async fn after_push_writes_commit_graph() { let tmp = tempfile::tempdir().expect("tempdir"); let env = env_for(tmp.path()); let bare = tmp.path().join("repo.git"); init_bare(&env, &bare, "main").await.expect("init bare"); let work = tmp.path().join("work"); std::fs::create_dir(&work).expect("mkdir work"); git(&work, &["init", "-b", "main"]); let bare_str = bare.to_str().expect("utf8 path"); for text in ["one", "two"] { std::fs::write(work.join("file.txt"), text).expect("write"); git(&work, &["add", "file.txt"]); git(&work, &["commit", "-m", text]); } git(&work, &["push", bare_str, "main"]); after_push(&env, &bare).await.expect("after_push"); let single = bare.join("objects/info/commit-graph"); let split = bare.join("objects/info/commit-graphs"); assert!( single.is_file() || split.is_dir(), "no commit-graph written under {}", bare.display() ); // A second run on an unchanged repo must also succeed. after_push(&env, &bare).await.expect("second after_push"); } #[tokio::test] async fn after_push_reports_missing_repo() { let tmp = tempfile::tempdir().expect("tempdir"); let env = env_for(tmp.path()); let missing = tmp.path().join("nope.git"); assert!(after_push(&env, &missing).await.is_err()); } }