use crate::ci;
use crate::config::Project;
use anyhow::Result;
use std::path::Path;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Kind {
Setup,
Lint,
Test,
}
impl Kind {
pub fn as_str(self) -> &'static str {
match self {
Kind::Setup => "setup",
Kind::Lint => "lint",
Kind::Test => "test",
}
}
}
#[derive(Debug, Clone)]
pub struct Resolved {
pub script: String,
pub source: &'static str,
pub caveats: Vec<String>,
}
pub fn resolve(project: &Project, dir: &Path, kind: Kind) -> Result<Option<Resolved>> {
let explicit = match kind {
Kind::Setup => &project.setup,
Kind::Lint => &project.lint,
Kind::Test => &project.test,
};
if let Some(s) = explicit {
return Ok(Some(Resolved {
script: s.clone(),
source: "override",
caveats: vec![],
}));
}
if let Some(cimap) = &project.ci {
let job = match kind {
Kind::Setup => cimap.setup_job.as_ref(),
Kind::Lint => cimap.lint_job.as_ref(),
Kind::Test => cimap.test_job.as_ref(),
};
if let Some(job) = job {
let wf = dir.join(".github/workflows").join(&cimap.workflow);
if wf.is_file() {
if let Some(ex) = ci::extract_job(&wf, job)? {
if !ex.script.is_empty() {
return Ok(Some(Resolved {
script: ex.script,
source: "ci",
caveats: ex.caveats,
}));
}
}
}
}
}
let hook = dir.join(".repoverse").join(kind.as_str());
if hook.is_file() {
return Ok(Some(Resolved {
script: format!("./.repoverse/{}", kind.as_str()),
source: "hook",
caveats: vec![],
}));
}
if let Some(script) = autodetect(dir, kind) {
return Ok(Some(Resolved {
script,
source: "autodetect",
caveats: vec![],
}));
}
Ok(None)
}
fn autodetect(dir: &Path, kind: Kind) -> Option<String> {
let has = |f: &str| dir.join(f).exists();
match kind {
Kind::Setup => {
if has("Cargo.toml") {
Some("cargo fetch".into())
} else if has("package-lock.json") {
Some("npm ci".into())
} else if has("pnpm-lock.yaml") {
Some("pnpm install --frozen-lockfile".into())
} else if has("uv.lock") {
Some("uv sync".into())
} else if has("go.mod") {
Some("go mod download".into())
} else if has("requirements.txt") {
Some("pip install -r requirements.txt".into())
} else {
None
}
}
Kind::Lint => {
if has("Cargo.toml") {
Some("cargo clippy --all-targets -- -D warnings".into())
} else if has("package.json") {
Some("npm run lint --if-present".into())
} else {
None
}
}
Kind::Test => {
if has("Cargo.toml") {
Some("cargo test".into())
} else if has("package.json") {
Some("npm test".into())
} else if has("go.mod") {
Some("go test ./...".into())
} else if has("pytest.ini") || has("pyproject.toml") {
Some("pytest -q".into())
} else {
None
}
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use tempfile::tempdir;
fn proj() -> Project {
serde_yaml::from_str("{ name: acme/x, path: x }").unwrap()
}
#[test]
fn override_wins() {
let mut p = proj();
p.test = Some("make t".into());
let d = tempdir().unwrap();
let r = resolve(&p, d.path(), Kind::Test).unwrap().unwrap();
assert_eq!(r.source, "override");
assert_eq!(r.script, "make t");
}
#[test]
fn autodetects_cargo() {
let d = tempdir().unwrap();
std::fs::write(d.path().join("Cargo.toml"), "").unwrap();
let r = resolve(&proj(), d.path(), Kind::Test).unwrap().unwrap();
assert_eq!(r.source, "autodetect");
assert_eq!(r.script, "cargo test");
}
#[test]
fn none_when_nothing() {
let d = tempdir().unwrap();
assert!(resolve(&proj(), d.path(), Kind::Lint).unwrap().is_none());
}
#[test]
fn hook_beats_autodetect() {
let d = tempdir().unwrap();
std::fs::write(d.path().join("Cargo.toml"), "").unwrap();
std::fs::create_dir_all(d.path().join(".repoverse")).unwrap();
std::fs::write(d.path().join(".repoverse/test"), "#!/bin/sh\n").unwrap();
let r = resolve(&proj(), d.path(), Kind::Test).unwrap().unwrap();
assert_eq!(r.source, "hook");
}
}