use std::fs;
use std::path::{Path, PathBuf};
use anyhow::{Context, Result};
pub const CONFIG_DIR_ENV: &str = "PATCHCOV_CONFIG_DIR";
pub const CONFIG_DIR_NAME: &str = ".patchcov";
pub fn resolve_config_dir_at(override_dir: Option<&Path>, repo_root: &Path) -> PathBuf {
let env = std::env::var(CONFIG_DIR_ENV).ok();
resolve_with_env(override_dir, repo_root, env.as_deref())
}
fn resolve_with_env(
override_dir: Option<&Path>,
repo_root: &Path,
env_dir: Option<&str>,
) -> PathBuf {
if let Some(dir) = override_dir {
return dir.to_path_buf();
}
if let Some(dir) = env_dir.filter(|dir| !dir.is_empty()) {
return PathBuf::from(dir);
}
walk_up_find_config_dir(repo_root).unwrap_or_else(|| repo_root.join(CONFIG_DIR_NAME))
}
fn walk_up_find_config_dir(start: &Path) -> Option<PathBuf> {
let mut current = start.to_path_buf();
loop {
let candidate = current.join(CONFIG_DIR_NAME);
if candidate.is_dir() {
return Some(candidate);
}
if current.join(".git").exists() || !current.pop() {
return None;
}
}
}
pub fn load_config_content(dir: &Path, filename: &str) -> Result<Option<String>> {
let path = dir.join(filename);
if !path.exists() {
return Ok(None);
}
fs::read_to_string(&path)
.with_context(|| format!("could not read config file {}", path.display()))
.map(Some)
}
#[cfg(test)]
#[allow(clippy::unwrap_used, clippy::expect_used)]
mod tests {
use super::*;
fn repo() -> tempfile::TempDir {
let dir = tempfile::tempdir().unwrap();
fs::create_dir(dir.path().join(".git")).unwrap();
dir
}
#[test]
fn override_wins_over_everything() {
let repo = repo();
fs::create_dir(repo.path().join(CONFIG_DIR_NAME)).unwrap();
let got = resolve_with_env(Some(Path::new("/x")), repo.path(), Some("/env"));
assert_eq!(got, Path::new("/x"));
}
#[test]
fn env_wins_over_discovery() {
let repo = repo();
fs::create_dir(repo.path().join(CONFIG_DIR_NAME)).unwrap();
let got = resolve_with_env(None, repo.path(), Some("/env"));
assert_eq!(got, Path::new("/env"));
}
#[test]
fn empty_env_is_ignored() {
let repo = repo();
let got = resolve_with_env(None, repo.path(), Some(""));
assert_eq!(got, repo.path().join(CONFIG_DIR_NAME));
}
#[test]
fn walk_up_finds_the_nearest_dir_within_the_repository() {
let repo = repo();
let nested = repo.path().join("a/b");
fs::create_dir_all(&nested).unwrap();
fs::create_dir(repo.path().join(CONFIG_DIR_NAME)).unwrap();
assert_eq!(
resolve_with_env(None, &nested, None),
repo.path().join(CONFIG_DIR_NAME)
);
}
#[test]
fn walk_up_does_not_escape_the_repository() {
let outer = tempfile::tempdir().unwrap();
fs::create_dir(outer.path().join(CONFIG_DIR_NAME)).unwrap();
let inner = outer.path().join("repo");
fs::create_dir_all(inner.join(".git")).unwrap();
assert_eq!(
resolve_with_env(None, &inner, None),
inner.join(CONFIG_DIR_NAME)
);
}
#[test]
fn load_returns_none_for_a_missing_file_and_content_otherwise() {
let dir = tempfile::tempdir().unwrap();
assert_eq!(
load_config_content(dir.path(), "config.yaml").unwrap(),
None
);
fs::write(dir.path().join("config.yaml"), "diff: {}\n").unwrap();
assert_eq!(
load_config_content(dir.path(), "config.yaml")
.unwrap()
.as_deref(),
Some("diff: {}\n")
);
}
}