use walkdir::WalkDir;
#[derive(Debug, Clone)]
pub struct JustfilePath {
pub dir: String,
pub path: String,
}
pub fn find_justfiles(repo_root_abs: &str) -> Result<Vec<JustfilePath>, String> {
let mut out = Vec::new();
let root = std::path::Path::new(repo_root_abs);
let walker = WalkDir::new(repo_root_abs)
.follow_links(false)
.into_iter()
.filter_entry(|e| {
if e.path() == root {
return true;
}
let name = e.file_name().to_string_lossy();
if e.file_type().is_dir() {
!name.starts_with('.')
} else {
true
}
});
for entry in walker.filter_map(std::result::Result::ok) {
if !entry.file_type().is_file() {
continue;
}
let name = entry.file_name().to_string_lossy();
if name.eq_ignore_ascii_case("justfile") {
let path_abs = entry
.path()
.canonicalize()
.map_err(|e| format!("Failed to canonicalize {}: {e}", entry.path().display()))?;
let dir_abs = path_abs
.parent()
.ok_or_else(|| format!("No parent directory for {}", path_abs.display()))?
.to_path_buf();
out.push(JustfilePath {
dir: dir_abs.to_string_lossy().to_string(),
path: path_abs.to_string_lossy().to_string(),
});
}
}
Ok(out)
}
#[cfg(test)]
#[expect(clippy::unwrap_used)]
mod tests {
use super::*;
use std::fs;
use tempfile::TempDir;
#[test]
fn finds_justfile_case_insensitive() {
let tmp = TempDir::new().unwrap();
let root = tmp.path();
fs::write(root.join("justfile"), "default:\n echo hi").unwrap();
fs::create_dir_all(root.join("sub")).unwrap();
fs::write(root.join("sub/Justfile"), "build:\n cargo build").unwrap();
let results = find_justfiles(root.to_str().unwrap()).unwrap();
assert_eq!(results.len(), 2);
}
#[test]
fn prunes_hidden_directories() {
let tmp = TempDir::new().unwrap();
let root = tmp.path();
fs::create_dir_all(root.join(".hidden")).unwrap();
fs::write(root.join(".hidden/justfile"), "secret:\n echo secret").unwrap();
fs::write(root.join("justfile"), "default:\n echo hi").unwrap();
let results = find_justfiles(root.to_str().unwrap()).unwrap();
assert_eq!(results.len(), 1);
assert!(results[0].path.contains("justfile"));
assert!(!results[0].path.contains(".hidden"));
}
#[test]
fn returns_absolute_paths() {
let tmp = TempDir::new().unwrap();
let root = tmp.path();
fs::write(root.join("justfile"), "default:\n echo hi").unwrap();
let results = find_justfiles(root.to_str().unwrap()).unwrap();
assert_eq!(results.len(), 1);
assert!(
std::path::Path::new(&results[0].path).is_absolute(),
"path should be absolute: {}",
results[0].path
);
assert!(
std::path::Path::new(&results[0].dir).is_absolute(),
"dir should be absolute: {}",
results[0].dir
);
}
}