Skip to main content

ev/
staleness.rs

1//! Resolve the staleness-reference sha per the configured policy. No network — `live-origin`
2//! reads the last-fetched upstream tracking ref. Returns None when the reference can't be
3//! determined ("stale-unknown"), in which case sha-staleness is simply not evaluated.
4use crate::store::Store;
5use std::path::Path;
6use std::process::Command;
7
8/// `git rev-parse <rev>` in `repo` → the 40-lower-hex sha, or None if it fails / is malformed.
9fn git_sha(repo: &Path, rev: &str) -> Option<String> {
10    let out = Command::new("git")
11        .args(["rev-parse", rev])
12        .current_dir(repo)
13        .output()
14        .ok()?;
15    if !out.status.success() {
16        return None;
17    }
18    let sha = String::from_utf8_lossy(&out.stdout).trim().to_string();
19    crate::tick::is_40_lower_hex(&sha).then_some(sha)
20}
21
22/// The staleness-reference sha per `policy` ("none" | "local-head" | anything else = live-origin).
23/// `offline` forces the cached reference (results/origin-sha) and never resolves fresh.
24pub fn resolve(repo: &Path, store: &Store, policy: &str, offline: bool) -> Option<String> {
25    if offline {
26        return store.read_origin_sha();
27    }
28    match policy {
29        "none" => None,
30        "local-head" => git_sha(repo, "HEAD"),
31        _ => {
32            // live-origin: the last-fetched upstream; cache it so an --offline run can reuse it.
33            let sha = git_sha(repo, "@{upstream}");
34            match &sha {
35                Some(s) => {
36                    let _ = store.write_origin_sha(s);
37                }
38                None => eprintln!(
39                    "warning: cannot resolve the live-origin staleness reference (no upstream?) — sha-staleness skipped"
40                ),
41            }
42            sha
43        }
44    }
45}
46
47#[cfg(test)]
48mod tests {
49    use super::*;
50    use crate::store::Store;
51
52    // A fresh git repo (one empty commit) + an ev store; returns (path, HEAD-sha).
53    fn git_store() -> (std::path::PathBuf, Store, String) {
54        use std::sync::atomic::{AtomicU64, Ordering};
55        static N: AtomicU64 = AtomicU64::new(0);
56        let p = std::env::temp_dir().join(format!(
57            "ev-staleness-{}-{}",
58            std::process::id(),
59            N.fetch_add(1, Ordering::Relaxed)
60        ));
61        let _ = std::fs::remove_dir_all(&p);
62        std::fs::create_dir_all(&p).unwrap();
63        for args in [
64            ["init"].as_slice(),
65            ["config", "user.email", "t@e.st"].as_slice(),
66            ["config", "user.name", "Tester"].as_slice(),
67            ["commit", "--allow-empty", "-m", "init"].as_slice(),
68        ] {
69            Command::new("git")
70                .args(args)
71                .current_dir(&p)
72                .output()
73                .unwrap();
74        }
75        let head = 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        let s = Store::at(&p);
87        s.init().unwrap();
88        (p, s, head)
89    }
90
91    #[test]
92    fn git_sha_should_return_the_head_when_run_in_a_git_repo() {
93        // given: a git repo with one commit
94        let (p, _s, head) = git_store();
95
96        // when: HEAD is resolved
97        let sha = git_sha(&p, "HEAD");
98
99        // then: it is the 40-hex HEAD sha
100        assert_eq!(sha.as_deref(), Some(head.as_str()));
101    }
102
103    #[test]
104    fn resolve_should_be_none_when_the_policy_is_none() {
105        // given: a git repo
106        let (p, s, _head) = git_store();
107
108        // when: resolved with policy "none"
109        let sha = resolve(&p, &s, "none", false);
110
111        // then: there is no staleness reference
112        assert!(sha.is_none());
113    }
114
115    #[test]
116    fn resolve_should_be_the_local_head_when_the_policy_is_local_head() {
117        // given: a git repo at a known HEAD
118        let (p, s, head) = git_store();
119
120        // when: resolved with policy "local-head"
121        let sha = resolve(&p, &s, "local-head", false);
122
123        // then: it is the local HEAD sha
124        assert_eq!(sha.as_deref(), Some(head.as_str()));
125    }
126
127    #[test]
128    fn resolve_should_use_the_cached_origin_when_offline() {
129        // given: a store with a cached origin-sha
130        let (p, s, _head) = git_store();
131        std::fs::write(
132            s.root.join("results").join("origin-sha"),
133            "d308afac1b2c3d4e5f60718293a4b5c6d7e8f901",
134        )
135        .unwrap();
136
137        // when: resolved offline (any policy)
138        let sha = resolve(&p, &s, "live-origin", true);
139
140        // then: it is the cached reference, resolved without git
141        assert_eq!(
142            sha.as_deref(),
143            Some("d308afac1b2c3d4e5f60718293a4b5c6d7e8f901")
144        );
145    }
146}