Wrap commit SHA and display ref into CommitRef struct
Ci::load now takes a CommitRef that bundles the full SHA (for git
operations) with a display form (for error messages). When no SHA
is given, ci validate shows @ instead of a short hash.

Assisted-by: GLM-5.1 via pi
change mopkqkpsyklsmklwqkytsmsmpxlkquyw
commit aae5af482f150d23ad267a3827b9c1f27b1cc96b
author Alpha Chen <alpha@kejadlen.dev>
date
parent ynqwrkoq
diff --git a/src/bin/quire/commands/ci.rs b/src/bin/quire/commands/ci.rs
index 224f609..9dec87c 100644
--- a/src/bin/quire/commands/ci.rs
+++ b/src/bin/quire/commands/ci.rs
@@ -1,23 +1,20 @@
 use std::path::PathBuf;
 
 use miette::{IntoDiagnostic, Result};
-use quire::ci::Ci;
+use quire::ci::{Ci, CommitRef};
 
 /// Validate a repo's ci.fnl without executing any jobs.
 ///
 /// Loads the Fennel source at the given SHA (or HEAD) to extract
 /// the job registration table, then runs the four structural validations.
 /// Prints each job found and any validation errors.
-pub async fn validate(sha: Option<&str>) -> Result<()> {
+pub async fn validate(maybe_sha: Option<&str>) -> Result<()> {
     let repo_path = discover_repo()?;
-    let sha = match sha {
-        Some(s) => s.to_string(),
-        None => current_commit()?,
-    };
+    let commit = resolve_commit(maybe_sha)?;
     let ci = Ci::new(repo_path);
 
-    let Some(pipeline) = ci.load(&sha)? else {
-        println!("No ci.fnl found at {sha}.");
+    let Some(pipeline) = ci.load(&commit)? else {
+        println!("No ci.fnl found at {}.", commit.display);
         return Ok(());
     };
 
@@ -64,6 +61,22 @@ pub async fn validate(sha: Option<&str>) -> Result<()> {
 }
 
 /// Find the repo root from the current working directory using jj.
+fn resolve_commit(maybe_sha: Option<&str>) -> Result<CommitRef> {
+    match maybe_sha {
+        Some(s) => Ok(CommitRef {
+            sha: s.to_string(),
+            display: s.to_string(),
+        }),
+        None => {
+            let sha = current_commit()?;
+            Ok(CommitRef {
+                sha,
+                display: "@".to_string(),
+            })
+        }
+    }
+}
+
 fn discover_repo() -> Result<PathBuf> {
     let output = std::process::Command::new("jj")
         .args(["root"])
diff --git a/src/ci/mod.rs b/src/ci/mod.rs
index f328a91..7e15ff7 100644
--- a/src/ci/mod.rs
+++ b/src/ci/mod.rs
@@ -6,6 +6,17 @@ pub mod run;
 pub use pipeline::{Job, Pipeline, ValidationError};
 pub use run::{Run, RunMeta, RunState, RunTimes, Runs};
 
+/// A resolved commit reference.
+///
+/// Carries both the full SHA (for git operations) and a short display
+/// form (for error messages and user-facing output).
+pub struct CommitRef {
+    /// Full commit SHA for git operations.
+    pub sha: String,
+    /// Short or human-readable form for display.
+    pub display: String,
+}
+
 use std::path::PathBuf;
 
 use crate::Result;
@@ -37,13 +48,14 @@ impl Ci {
     /// Load ci.fnl at a given SHA and return the parsed pipeline.
     ///
     /// Returns `Ok(None)` if the repo has no ci.fnl at that commit.
-    pub fn load(&self, sha: &str) -> Result<Option<Pipeline>> {
-        let Some(source) = self.source(sha)? else {
+    pub fn load(&self, commit: &CommitRef) -> Result<Option<Pipeline>> {
+        let Some(source) = self.source(&commit.sha)? else {
             return Ok(None);
         };
         let fennel = crate::fennel::Fennel::new()?;
-        let name = format!("{sha}:{CI_FNL}");
-        let pipeline = pipeline::load(&fennel, &source, &name)?;
+        let name = CI_FNL.to_string();
+        let lua_name = format!("{}:{CI_FNL}", commit.sha);
+        let pipeline = pipeline::load(&fennel, &source, &lua_name, &name)?;
         Ok(Some(pipeline))
     }
 
@@ -132,8 +144,9 @@ fn trigger_ref(repo: &Repo, pushed_at: jiff::Timestamp, push_ref: &PushRef) -> R
     run.transition(RunState::Active)?;
 
     let fennel = crate::fennel::Fennel::new()?;
-    let name = format!("{}:{CI_FNL}", push_ref.new_sha);
-    let pipeline = match pipeline::load(&fennel, &source, &name) {
+    let name = CI_FNL.to_string();
+    let lua_name = format!("{}:{CI_FNL}", push_ref.new_sha);
+    let pipeline = match pipeline::load(&fennel, &source, &lua_name, &name) {
         Ok(r) => r,
         Err(e) => {
             run.transition(RunState::Failed)?;
diff --git a/src/ci/pipeline.rs b/src/ci/pipeline.rs
index 0ae0ed8..392dff6 100644
--- a/src/ci/pipeline.rs
+++ b/src/ci/pipeline.rs
@@ -69,10 +69,15 @@ impl UserData for CiModule {
 ///
 /// Injects `quire.ci` into `package.loaded` so scripts can
 /// `(require :quire.ci)`, evaluates the source, and takes the accumulated jobs.
-pub(crate) fn load(fennel: &Fennel, source: &str, name: &str) -> Result<Pipeline> {
+pub(crate) fn load(
+    fennel: &Fennel,
+    source: &str,
+    filename: &str,
+    display: &str,
+) -> Result<Pipeline> {
     let jobs = Rc::new(RefCell::new(Vec::new()));
 
-    fennel.eval_raw(source, name, |lua| {
+    fennel.eval_raw(source, filename, display, |lua| {
         let loaded: mlua::Table = lua.globals().get::<mlua::Table>("package")?.get("loaded")?;
         loaded.set("quire.ci", CiModule { jobs: jobs.clone() })?;
         Ok(())
@@ -207,7 +212,7 @@ mod tests {
         let f = fennel();
         let source = r#"(local ci (require :quire.ci))
 (ci:job :test [:quire/push] (fn [_] nil))"#;
-        let result = load(&f, source, "ci.fnl").expect("eval should succeed");
+        let result = load(&f, source, "ci.fnl", "ci.fnl").expect("eval should succeed");
         assert_eq!(result.jobs.len(), 1);
         assert_eq!(result.jobs[0].id, "test");
         assert_eq!(result.jobs[0].inputs, vec!["quire/push"]);
@@ -221,7 +226,7 @@ mod tests {
 (ci:job :build [:quire/push] (fn [_] nil))
 (ci:job :test [:build] (fn [_] nil))
 "#;
-        let result = load(&f, source, "ci.fnl").expect("eval should succeed");
+        let result = load(&f, source, "ci.fnl", "ci.fnl").expect("eval should succeed");
         assert_eq!(result.jobs.len(), 2);
         assert_eq!(result.jobs[0].id, "build");
         assert_eq!(result.jobs[0].inputs, vec!["quire/push"]);
@@ -232,7 +237,7 @@ mod tests {
     #[test]
     fn load_errors_on_bad_fennel() {
         let f = fennel();
-        let result = load(&f, "{:bad {:}", "ci.fnl");
+        let result = load(&f, "{:bad {:}", "ci.fnl", "ci.fnl");
         assert!(result.is_err(), "malformed Fennel should fail");
     }
 
@@ -244,7 +249,7 @@ mod tests {
 (ci:job :build [:quire/push] (fn [_] nil))
 (ci:job :test [:build :quire/push] (fn [_] nil))
 "#;
-        let pipeline = load(&f, source, "ci.fnl").expect("eval should succeed");
+        let pipeline = load(&f, source, "ci.fnl", "ci.fnl").expect("eval should succeed");
         assert!(pipeline.validate().is_ok());
     }
 
@@ -256,7 +261,7 @@ mod tests {
 (ci:job :a [:b] (fn [_] nil))
 (ci:job :b [:a] (fn [_] nil))
 "#;
-        let pipeline = load(&f, source, "ci.fnl").expect("eval should succeed");
+        let pipeline = load(&f, source, "ci.fnl", "ci.fnl").expect("eval should succeed");
         let errs = pipeline.validate().unwrap_err();
         assert!(
             errs.iter().any(|e| matches!(e, ValidationError::Cycle { cycle_jobs } if cycle_jobs.contains(&"a".to_string()) && cycle_jobs.contains(&"b".to_string()))),
@@ -273,7 +278,7 @@ mod tests {
 (ci:job :b [:a :quire/push] (fn [_] nil))
 (ci:job :clean [:quire/push] (fn [_] nil))
 "#;
-        let pipeline = load(&f, source, "ci.fnl").expect("eval should succeed");
+        let pipeline = load(&f, source, "ci.fnl", "ci.fnl").expect("eval should succeed");
         let errs = pipeline.validate().unwrap_err();
         let cycle_errs: Vec<&Vec<String>> = errs
             .iter()
@@ -300,7 +305,7 @@ mod tests {
 (ci:job :c [:d :quire/push] (fn [_] nil))
 (ci:job :d [:c :quire/push] (fn [_] nil))
 "#;
-        let pipeline = load(&f, source, "ci.fnl").expect("eval should succeed");
+        let pipeline = load(&f, source, "ci.fnl", "ci.fnl").expect("eval should succeed");
         let errs = pipeline.validate().unwrap_err();
         let cycle_count = errs
             .iter()
@@ -314,7 +319,7 @@ mod tests {
         let f = fennel();
         let source = r#"(local ci (require :quire.ci))
 (ci:job :setup [] (fn [_] nil))"#;
-        let pipeline = load(&f, source, "ci.fnl").expect("eval should succeed");
+        let pipeline = load(&f, source, "ci.fnl", "ci.fnl").expect("eval should succeed");
         let errs = pipeline.validate().unwrap_err();
         assert!(
             errs.iter()
@@ -328,7 +333,7 @@ mod tests {
         let f = fennel();
         let source = r#"(local ci (require :quire.ci))
 (ci:job :orphan [:orphan] (fn [_] nil))"#;
-        let pipeline = load(&f, source, "ci.fnl").expect("eval should succeed");
+        let pipeline = load(&f, source, "ci.fnl", "ci.fnl").expect("eval should succeed");
         let errs = pipeline.validate().unwrap_err();
         assert!(
             errs.iter().any(
@@ -343,7 +348,7 @@ mod tests {
         let f = fennel();
         let source = r#"(local ci (require :quire.ci))
 (ci:job :foo/bar [:quire/push] (fn [_] nil))"#;
-        let pipeline = load(&f, source, "ci.fnl").expect("eval should succeed");
+        let pipeline = load(&f, source, "ci.fnl", "ci.fnl").expect("eval should succeed");
         let errs = pipeline.validate().unwrap_err();
         assert!(
             errs.iter().any(
diff --git a/src/fennel.rs b/src/fennel.rs
index 1c68774..162a09e 100644
--- a/src/fennel.rs
+++ b/src/fennel.rs
@@ -76,15 +76,18 @@ impl Fennel {
     /// Lua value.
     ///
     /// `setup` is called before evaluation and can inject globals or
-    /// modify the VM. `name` is used in error messages — typically a
-    /// filename or a synthetic label like `HEAD:.quire/config.fnl`.
+    /// modify the VM.
+    ///
+    /// `filename` is passed to Lua as the source name (used in tracebacks).
+    /// `display` is used in miette error messages (can be more human-readable).
     pub fn eval_raw(
         &self,
         source: &str,
-        name: &str,
+        filename: &str,
+        display: &str,
         setup: impl Fn(&Lua) -> mlua::Result<()>,
     ) -> Result<mlua::Value, FennelError> {
-        setup(&self.lua).map_err(|e| FennelError::from_lua(source, name, e))?;
+        setup(&self.lua).map_err(|e| FennelError::from_lua(source, display, e))?;
 
         let fennel: mlua::Table = self.lua.globals().get("fennel")?;
 
@@ -92,11 +95,11 @@ impl Fennel {
 
         let opts = self.lua.create_table()?;
 
-        opts.set("filename", name)?;
+        opts.set("filename", filename)?;
 
         let result = eval
             .call::<mlua::Value>((source, opts))
-            .map_err(|e| FennelError::from_lua(source, name, e))?;
+            .map_err(|e| FennelError::from_lua(source, display, e))?;
 
         Ok(result)
     }
@@ -116,7 +119,7 @@ impl Fennel {
             });
         }
 
-        let result = self.eval_raw(source, name, |_| Ok(()))?;
+        let result = self.eval_raw(source, name, name, |_| Ok(()))?;
 
         // Reject nil results — a config file that evaluates to nothing is
         // almost always a mistake.