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        });
79    }
80    if let Some(env_ref) = env_override {
81        validate(&env_ref, AuditBaseOrigin::Environment)?;
82        return Ok(ResolvedAuditBase {
83            description: Some(format!("FALLOW_AUDIT_BASE={env_ref}")),
84            git_ref: env_ref,
85        });
86    }
87    let detected =
88        repo_refs::auto_detect_audit_base_ref(root).ok_or(AuditBaseError::NotDetected)?;
89    validate(&detected.git_ref, AuditBaseOrigin::Detected)?;
90    Ok(detected)
91}
92
93fn validate(value: &str, origin: AuditBaseOrigin) -> Result<(), AuditBaseError> {
94    fallow_engine::validate::validate_git_ref(value)
95        .map(|_| ())
96        .map_err(|reason| AuditBaseError::InvalidRef {
97            origin,
98            value: value.to_string(),
99            reason,
100        })
101}
102
103#[cfg(test)]
104mod tests {
105    use super::*;
106
107    #[test]
108    fn the_override_is_trimmed_and_an_empty_one_is_unset() {
109        assert_eq!(parse_audit_base_override(None), None);
110        assert_eq!(parse_audit_base_override(Some(String::new())), None);
111        assert_eq!(parse_audit_base_override(Some("   ".to_string())), None);
112        assert_eq!(
113            parse_audit_base_override(Some("  origin/main  ".to_string())),
114            Some("origin/main".to_string())
115        );
116    }
117
118    #[test]
119    fn an_explicit_ref_wins_over_the_override() {
120        let resolved = resolve_audit_base_with_override(
121            Path::new("."),
122            Some("main"),
123            Some("upstream/main".to_string()),
124        )
125        .expect("explicit ref resolves");
126        assert_eq!(resolved.git_ref, "main");
127        assert_eq!(resolved.description, None);
128    }
129
130    #[test]
131    fn the_override_names_its_source() {
132        let resolved = resolve_audit_base_with_override(
133            Path::new("."),
134            None,
135            Some("upstream/main".to_string()),
136        )
137        .expect("override resolves");
138        assert_eq!(resolved.git_ref, "upstream/main");
139        assert_eq!(
140            resolved.description.as_deref(),
141            Some("FALLOW_AUDIT_BASE=upstream/main")
142        );
143    }
144
145    #[test]
146    fn an_invalid_override_names_its_origin() {
147        let error = resolve_audit_base_with_override(
148            Path::new("."),
149            None,
150            Some("--upload-pack=evil".to_string()),
151        )
152        .expect_err("an option-like ref is refused");
153        assert!(
154            matches!(
155                error,
156                AuditBaseError::InvalidRef {
157                    origin: AuditBaseOrigin::Environment,
158                    ..
159                }
160            ),
161            "{error:?}"
162        );
163    }
164}