forge
ExploreLog in

Tahinli/forge

public
0 watching
★ 0
3.7 KiB
1//! Post-push housekeeping for a bare repository: commit-graph refresh and `git maintenance --auto`.
2
3use std::path::Path;
4use std::time::Duration;
5
6use crate::command::{GitCommand, GitEnv};
7use crate::error::{Error, Result};
8
9/// Upper bound for each git invocation. The child is killed when the timeout fires.
10const STEP_TIMEOUT: Duration = Duration::from_secs(120);
11
12/// Refreshes the commit-graph (with changed-path filters) and runs the auto maintenance tasks
13/// for the bare repo at `repo`. Each step is bounded by [`STEP_TIMEOUT`].
14pub async fn after_push(env: &GitEnv, repo: &Path) -> Result<()> {
15 let graph = GitCommand::new(env, "commit-graph")
16 .git_dir(repo)
17 .arg("write")
18 .arg("--reachable")
19 .arg("--changed-paths");
20 run_bounded(graph).await?;
21
22 let maintenance = GitCommand::new(env, "maintenance")
23 .git_dir(repo)
24 .arg("run")
25 .arg("--auto")
26 .arg("--quiet");
27 run_bounded(maintenance).await
28}
29
30/// Runs `cmd` with [`STEP_TIMEOUT`]. Dropping the future on timeout drops the child,
31/// which `GitCommand` spawns with `kill_on_drop`, so the process is killed.
32async fn run_bounded(cmd: GitCommand) -> Result<()> {
33 match tokio::time::timeout(STEP_TIMEOUT, cmd.status()).await {
34 Ok(result) => result,
35 Err(_) => Err(Error::Failed(format!(
36 "timed out after {}s",
37 STEP_TIMEOUT.as_secs()
38 ))),
39 }
40}
41
42#[cfg(test)]
43mod tests {
44 use super::*;
45 use crate::repo::init_bare;
46 use std::path::PathBuf;
47
48 fn git(dir: &Path, args: &[&str]) {
49 let out = std::process::Command::new("git")
50 .arg("-C")
51 .arg(dir)
52 .args(args)
53 .env("GIT_AUTHOR_NAME", "test")
54 .env("GIT_AUTHOR_EMAIL", "test@example.com")
55 .env("GIT_COMMITTER_NAME", "test")
56 .env("GIT_COMMITTER_EMAIL", "test@example.com")
57 .output()
58 .expect("git runs");
59 assert!(
60 out.status.success(),
61 "git {args:?} failed: {}",
62 String::from_utf8_lossy(&out.stderr)
63 );
64 }
65
66 fn env_for(tmp: &Path) -> GitEnv {
67 GitEnv {
68 git_bin: PathBuf::from("git"),
69 data_dir: tmp.to_path_buf(),
70 }
71 }
72
73 #[tokio::test]
74 async fn after_push_writes_commit_graph() {
75 let tmp = tempfile::tempdir().expect("tempdir");
76 let env = env_for(tmp.path());
77 let bare = tmp.path().join("repo.git");
78 init_bare(&env, &bare, "main").await.expect("init bare");
79
80 let work = tmp.path().join("work");
81 std::fs::create_dir(&work).expect("mkdir work");
82 git(&work, &["init", "-b", "main"]);
83 let bare_str = bare.to_str().expect("utf8 path");
84 for text in ["one", "two"] {
85 std::fs::write(work.join("file.txt"), text).expect("write");
86 git(&work, &["add", "file.txt"]);
87 git(&work, &["commit", "-m", text]);
88 }
89 git(&work, &["push", bare_str, "main"]);
90
91 after_push(&env, &bare).await.expect("after_push");
92
93 let single = bare.join("objects/info/commit-graph");
94 let split = bare.join("objects/info/commit-graphs");
95 assert!(
96 single.is_file() || split.is_dir(),
97 "no commit-graph written under {}",
98 bare.display()
99 );
100
101 // A second run on an unchanged repo must also succeed.
102 after_push(&env, &bare).await.expect("second after_push");
103 }
104
105 #[tokio::test]
106 async fn after_push_reports_missing_repo() {
107 let tmp = tempfile::tempdir().expect("tempdir");
108 let env = env_for(tmp.path());
109 let missing = tmp.path().join("nope.git");
110 assert!(after_push(&env, &missing).await.is_err());
111 }
112}