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
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.