Skip to main content

fallow_api/audit_run/
base_ref.rs

1//! The base ref that an audit compares against.
2
3use std::path::Path;
4
5use fallow_engine::repo_refs::{self, ResolvedAuditBase};
6
7/// Where an audit base ref came from.
8#[derive(Debug, Clone, Copy, PartialEq, Eq)]
9pub enum AuditBaseOrigin {
10    /// The caller named the ref (`--base`, `audit.base`, `changedSince`).
11    Explicit,
12    /// The `FALLOW_AUDIT_BASE` environment variable.
13    Environment,
14    /// Auto-detection from the upstream or the remote default branch.
15    Detected,
16}
17
18/// Why no audit base ref could be resolved.
19#[derive(Debug, Clone, PartialEq, Eq)]
20pub enum AuditBaseError {
21    /// The ref is not a valid git ref.
22    InvalidRef {
23        /// Where the ref came from.
24        origin: AuditBaseOrigin,
25        /// The ref as it was given or detected.
26        value: String,
27        /// Why the ref is not valid.
28        reason: String,
29    },
30    /// No explicit ref, no environment override, and no base branch to
31    /// detect.
32    NotDetected,
33}
34
35/// Parse a raw `FALLOW_AUDIT_BASE` value: trimmed, and `None` when it is
36/// empty or only whitespace.
37#[must_use]
38pub fn parse_audit_base_override(raw: Option<String>) -> Option<String> {
39    let trimmed = raw?.trim().to_string();
40    if trimmed.is_empty() {
41        None
42    } else {
43        Some(trimmed)
44    }
45}
46
47/// Resolve the base ref of an audit rooted at `root`.
48///
49/// The order is: the explicit ref, then the `FALLOW_AUDIT_BASE` override
50/// (issue #1168: a consumer can pin the base without editing a generated gate
51/// script), then auto-detection. Each ref is validated before git sees it.
52///
53/// # Errors
54///
55/// Returns [`AuditBaseError::InvalidRef`] for a ref that is not a valid git
56/// ref, and [`AuditBaseError::NotDetected`] when no base branch can be found.
57pub fn resolve_audit_base(
58    root: &Path,
59    explicit: Option<&str>,
60) -> Result<ResolvedAuditBase, AuditBaseError> {
61    resolve_audit_base_with_override(
62        root,
63        explicit,
64        parse_audit_base_override(std::env::var("FALLOW_AUDIT_BASE").ok()),
65    )
66}
67
68fn resolve_audit_base_with_override(
69    root: &Path,
70    explicit: Option<&str>,
71    env_override: Option<String>,
72) -> Result<ResolvedAuditBase, AuditBaseError> {
73    if let Some(explicit) = explicit {
74        validate(explicit, AuditBaseOrigin::Explicit)?;
75        return Ok(ResolvedAuditBase {
76            git_ref: explicit.to_string(),
77            description: None,
78            inherited_refs: Vec::new(),
79        });
80    }
81    if let Some(env_ref) = env_override {
82        validate(&env_ref, AuditBaseOrigin::Environment)?;
83        return Ok(ResolvedAuditBase {
84            description: Some(format!("FALLOW_AUDIT_BASE={env_ref}")),
85            git_ref: env_ref,
86            inherited_refs: Vec::new(),
87        });
88    }
89    let detected =
90        repo_refs::auto_detect_audit_base_ref(root).ok_or(AuditBaseError::NotDetected)?;
91    validate(&detected.git_ref, AuditBaseOrigin::Detected)?;
92    Ok(detected)
93}
94
95fn validate(value: &str, origin: AuditBaseOrigin) -> Result<(), AuditBaseError> {
96    fallow_engine::validate::validate_git_ref(value)
97        .map(|_| ())
98        .map_err(|reason| AuditBaseError::InvalidRef {
99            origin,
100            value: value.to_string(),
101            reason,
102        })
103}
104
105#[cfg(test)]
106mod tests {
107    use super::*;
108
109    #[test]
110    fn the_override_is_trimmed_and_an_empty_one_is_unset() {
111        assert_eq!(parse_audit_base_override(None), None);
112        assert_eq!(parse_audit_base_override(Some(String::new())), None);
113        assert_eq!(parse_audit_base_override(Some("   ".to_string())), None);
114        assert_eq!(
115            parse_audit_base_override(Some("  origin/main  ".to_string())),
116            Some("origin/main".to_string())
117        );
118    }
119
120    #[test]
121    fn an_explicit_ref_wins_over_the_override() {
122        let resolved = resolve_audit_base_with_override(
123            Path::new("."),
124            Some("main"),
125            Some("upstream/main".to_string()),
126        )
127        .expect("explicit ref resolves");
128        assert_eq!(resolved.git_ref, "main");
129        assert_eq!(resolved.description, None);
130    }
131
132    #[test]
133    fn the_override_names_its_source() {
134        let resolved = resolve_audit_base_with_override(
135            Path::new("."),
136            None,
137            Some("upstream/main".to_string()),
138        )
139        .expect("override resolves");
140        assert_eq!(resolved.git_ref, "upstream/main");
141        assert_eq!(
142            resolved.description.as_deref(),
143            Some("FALLOW_AUDIT_BASE=upstream/main")
144        );
145    }
146
147    #[test]
148    fn an_invalid_override_names_its_origin() {
149        let error = resolve_audit_base_with_override(
150            Path::new("."),
151            None,
152            Some("--upload-pack=evil".to_string()),
153        )
154        .expect_err("an option-like ref is refused");
155        assert!(
156            matches!(
157                error,
158                AuditBaseError::InvalidRef {
159                    origin: AuditBaseOrigin::Environment,
160                    ..
161                }
162            ),
163            "{error:?}"
164        );
165    }
166}