Skip to main content

ev/
liveness.rs

1//! Event-driven liveness: has a triggering change landed since a check last ran?
2//! Impure (shells git), mirroring `staleness.rs`. The verdict engine stays pure — this
3//! produces a bool the caller passes into `verdict_for`.
4use std::path::Path;
5use std::process::Command;
6
7/// True if any commit reachable from HEAD and NEWER than `since_commit` touches one of
8/// `paths` (a `triggered_by` set). `None` if git fails or `since_commit` is unknown — in
9/// which case event-driven staleness is simply NOT evaluated (never a false not-green).
10pub fn changed_since(repo: &Path, since_commit: &str, paths: &[String]) -> Option<bool> {
11    if paths.is_empty() {
12        return Some(false);
13    }
14    let mut args: Vec<String> = vec![
15        "rev-list".into(),
16        format!("{since_commit}..HEAD"),
17        "--".into(),
18    ];
19    args.extend(paths.iter().cloned());
20    let out = Command::new("git")
21        .args(&args)
22        .current_dir(repo)
23        .output()
24        .ok()?;
25    if !out.status.success() {
26        return None; // unknown commit / not a git repo → do not evaluate
27    }
28    Some(!out.stdout.is_empty())
29}
30
31#[cfg(test)]
32mod tests {
33    use super::*;
34    use std::process::Command;
35
36    // A git repo with two commits; returns (path, first_sha, second_sha). The second commit
37    // touches `pyproject.toml`; the first touches `readme.md`.
38    fn two_commit_repo() -> (std::path::PathBuf, String, String) {
39        use std::sync::atomic::{AtomicU64, Ordering};
40        static N: AtomicU64 = AtomicU64::new(0);
41        let p = std::env::temp_dir().join(format!(
42            "ev-liveness-{}-{}",
43            std::process::id(),
44            N.fetch_add(1, Ordering::Relaxed)
45        ));
46        let _ = std::fs::remove_dir_all(&p);
47        std::fs::create_dir_all(&p).unwrap();
48        let git = |args: &[&str]| {
49            Command::new("git")
50                .args(args)
51                .current_dir(&p)
52                .output()
53                .unwrap();
54        };
55        git(&["init"]);
56        git(&["config", "user.email", "t@e.st"]);
57        git(&["config", "user.name", "Tester"]);
58        std::fs::write(p.join("readme.md"), "a").unwrap();
59        git(&["add", "."]);
60        git(&["commit", "-m", "first"]);
61        let first = String::from_utf8(
62            Command::new("git")
63                .args(["rev-parse", "HEAD"])
64                .current_dir(&p)
65                .output()
66                .unwrap()
67                .stdout,
68        )
69        .unwrap()
70        .trim()
71        .to_string();
72        std::fs::write(p.join("pyproject.toml"), "deps=[]").unwrap();
73        git(&["add", "."]);
74        git(&["commit", "-m", "second touches pyproject"]);
75        let second = String::from_utf8(
76            Command::new("git")
77                .args(["rev-parse", "HEAD"])
78                .current_dir(&p)
79                .output()
80                .unwrap()
81                .stdout,
82        )
83        .unwrap()
84        .trim()
85        .to_string();
86        (p, first, second)
87    }
88
89    #[test]
90    fn changed_since_should_be_true_when_a_triggered_path_changed_after_the_commit() {
91        // given: a repo whose second commit touched pyproject.toml, evaluated from the first commit
92        let (repo, first, _second) = two_commit_repo();
93
94        // when: we ask whether pyproject.toml changed since the first commit
95        let r = changed_since(&repo, &first, &["pyproject.toml".into()]);
96
97        // then: yes — a triggering change landed after it
98        assert_eq!(r, Some(true));
99    }
100
101    #[test]
102    fn changed_since_should_be_false_when_no_triggered_path_changed_after_the_commit() {
103        // given: the same repo evaluated from its HEAD (second) commit
104        let (repo, _first, second) = two_commit_repo();
105
106        // when: we ask whether pyproject.toml changed since HEAD
107        let r = changed_since(&repo, &second, &["pyproject.toml".into()]);
108
109        // then: no — nothing landed after HEAD
110        assert_eq!(r, Some(false));
111    }
112
113    #[test]
114    fn changed_since_should_be_none_when_the_commit_is_unknown() {
115        // given: a repo and a sha that is not in its history
116        let (repo, _first, _second) = two_commit_repo();
117
118        // when: we probe from an unknown commit
119        let r = changed_since(
120            &repo,
121            "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
122            &["pyproject.toml".into()],
123        );
124
125        // then: it is None — unknown ⇒ event-driven staleness is simply not evaluated
126        assert_eq!(r, None);
127    }
128}