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