Merge has_ci_fnl and ci_fnl_source into one function
ci_fnl_source now returns Option<String> — None if the file does not
exist at the given SHA, Some(contents) if it does. Removes the
redundant has_ci_fnl check that preceded every ci_fnl_source call.

Assisted-by: GLM-5.1 via pi
change rwpowywmprypspxwmnwvrwulyuxppqyu
commit 566d2173463673610d5f60835d223aa0aece9ed5
author Alpha Chen <alpha@kejadlen.dev>
date
parent lpvuklzz
diff --git a/src/ci/mod.rs b/src/ci/mod.rs
index 59255d4..189d65d 100644
--- a/src/ci/mod.rs
+++ b/src/ci/mod.rs
@@ -35,19 +35,27 @@ impl Ci {
     }
 
     /// Evaluate ci.fnl at a given SHA and return the registration table.
-    pub fn eval(&self, sha: &str) -> Result<EvalResult> {
-        let source = self.ci_fnl_source(sha)?;
+    ///
+    /// Returns `Ok(None)` if the repo has no ci.fnl at that commit.
+    pub fn eval(&self, sha: &str) -> Result<Option<EvalResult>> {
+        let Some(source) = self.ci_fnl_source(sha)? else {
+            return Ok(None);
+        };
         let fennel = crate::fennel::Fennel::new()?;
         let name = format!("{sha}:{CI_FNL}");
         let result = eval_ci(&fennel, &source, &name)?;
-        Ok(result)
+        Ok(Some(result))
     }
 
     /// Evaluate ci.fnl at a given SHA and validate the job graph.
-    pub fn validate_at(&self, sha: &str) -> Result<EvalResult> {
-        let result = self.eval(sha)?;
+    ///
+    /// Returns `Ok(None)` if the repo has no ci.fnl at that commit.
+    pub fn validate_at(&self, sha: &str) -> Result<Option<EvalResult>> {
+        let Some(result) = self.eval(sha)? else {
+            return Ok(None);
+        };
         validate(&result.jobs)?;
-        Ok(result)
+        Ok(Some(result))
     }
 
     /// Evaluate a ci.fnl file from disk and validate the job graph.
@@ -60,18 +68,11 @@ impl Ci {
         Ok(result)
     }
 
-    /// Check whether this bare repo has `.quire/ci.fnl` at a given commit SHA.
-    fn has_ci_fnl(&self, sha: &str) -> bool {
-        self.git(&["show", &format!("{sha}:{CI_FNL}")])
-            .stdout(std::process::Stdio::null())
-            .stderr(std::process::Stdio::null())
-            .status()
-            .map(|s| s.success())
-            .unwrap_or(false)
-    }
-
     /// Read the contents of `.quire/ci.fnl` at a given commit SHA.
-    fn ci_fnl_source(&self, sha: &str) -> Result<String> {
+    ///
+    /// Returns `Ok(None)` if the file does not exist at that commit,
+    /// `Ok(Some(contents))` if it does, or `Err` for unexpected failures.
+    fn ci_fnl_source(&self, sha: &str) -> Result<Option<String>> {
         let output = self
             .git(&["show", &format!("{sha}:{CI_FNL}")])
             .stdout(std::process::Stdio::piped())
@@ -79,13 +80,19 @@ impl Ci {
             .output()?;
 
         if !output.status.success() {
+            // Distinguish "file not found" from real errors. git show
+            // exits 128 for missing paths but the stderr contains
+            // "does not exist" — check for that pattern.
             let stderr = String::from_utf8_lossy(&output.stderr);
+            if stderr.contains("does not exist") || stderr.contains("not found") {
+                return Ok(None);
+            }
             return Err(crate::Error::Git(format!(
                 "failed to read {CI_FNL} at {sha}: {stderr}"
             )));
         }
 
-        Ok(String::from_utf8(output.stdout)?)
+        Ok(Some(String::from_utf8(output.stdout)?))
     }
 
     /// Start a git command rooted in this repo.
@@ -127,9 +134,9 @@ pub fn trigger(quire: &crate::Quire, event: &PushEvent) {
 fn trigger_ref(repo: &Repo, pushed_at: jiff::Timestamp, push_ref: &PushRef) -> Result<()> {
     let ci = repo.ci();
 
-    if !ci.has_ci_fnl(&push_ref.new_sha) {
+    let Some(source) = ci.ci_fnl_source(&push_ref.new_sha)? else {
         return Ok(());
-    }
+    };
 
     let meta = RunMeta {
         sha: push_ref.new_sha.clone(),
@@ -148,9 +155,19 @@ fn trigger_ref(repo: &Repo, pushed_at: jiff::Timestamp, push_ref: &PushRef) -> R
 
     run.transition(RunState::Active)?;
 
-    let result = ci.validate_at(&push_ref.new_sha);
-    match result {
-        Ok(_) => {
+    // Evaluate and validate the source we already read.
+    let fennel = crate::fennel::Fennel::new()?;
+    let name = format!("{}:{CI_FNL}", push_ref.new_sha);
+    let result = match eval_ci(&fennel, &source, &name) {
+        Ok(r) => r,
+        Err(e) => {
+            run.transition(RunState::Failed)?;
+            return Err(e);
+        }
+    };
+
+    match validate(&result.jobs) {
+        Ok(()) => {
             run.transition(RunState::Complete)?;
         }
         Err(e) => {