use std::path::{Path, PathBuf};
#[must_use]
pub fn prefix() -> String {
std::env::var("VISSUE_PREFIX")
.ok()
.map(|raw| raw.trim().to_string())
.filter(|raw| !raw.is_empty())
.unwrap_or_else(|| vissue_core::DEFAULT_PREFIX.to_string())
}
#[must_use]
pub fn named_tracker(dir: &Path) -> bool {
dir.join("vissue.toml").is_file()
}
#[must_use]
pub fn holds_a_tracker(dir: &Path) -> bool {
named_tracker(dir) || dir.join(prefix()).is_dir()
}
#[must_use]
pub fn work_tree(dir: &Path) -> Option<PathBuf> {
let mut at = Some(dir);
while let Some(here) = at {
if here.join(".git").exists() {
return Some(here.to_path_buf());
}
at = here.parent();
}
None
}
#[must_use]
pub fn product_repo(root: &Path) -> Option<PathBuf> {
let top = work_tree(root)?;
(top != root && !holds_a_tracker(&top)).then_some(top)
}
#[must_use]
pub fn kept_out(repo: &Path, root: &Path) -> bool {
crate::git_cmd()
.arg("-C")
.arg(repo)
.arg("check-ignore")
.arg("-q")
.arg("--no-index")
.arg(root)
.stdin(std::process::Stdio::null())
.stdout(std::process::Stdio::null())
.stderr(std::process::Stdio::null())
.status()
.is_ok_and(|status| status.success())
}
#[must_use]
pub fn refusal(root: &Path, repo: &Path) -> String {
format!(
"{} is inside the checkout at {}, and a tracker is not that project's files; \
set VISSUE_ROOT=DIR outside it, or name one in {}",
root.display(),
repo.display(),
config_path()
.map(|p| p.display().to_string())
.unwrap_or_else(|| "vissue/config.toml".into())
)
}
#[must_use]
pub fn config_path() -> Option<PathBuf> {
std::env::var_os("VISSUE_CONFIG")
.filter(|raw| !raw.is_empty())
.map(PathBuf::from)
.or_else(|| crate::config_base().map(|b| b.join("vissue").join("config.toml")))
}
#[must_use]
pub fn root_line(text: &str) -> Option<&str> {
text.lines()
.take_while(|line| !line.trim_start().starts_with('['))
.find(|line| {
line.split('=')
.next()
.is_some_and(|key| key.trim() == "root")
})
}
#[must_use]
pub fn configured() -> Option<PathBuf> {
let text = std::fs::read_to_string(config_path()?).ok()?;
let value: toml::Value = toml::from_str(root_line(&text)?).ok()?;
let named = value.get("root")?.as_str()?.trim();
(!named.is_empty()).then(|| crate::expand(named))
}
#[must_use]
pub fn named_in_env() -> bool {
["ISSUE_ROOT", "VISSUE_ROOT"]
.iter()
.any(|key| std::env::var_os(key).is_some_and(|raw| !raw.is_empty()))
}
pub fn bind_root(cmd: &mut std::process::Command) {
if named_in_env() {
return;
}
let here = std::env::current_dir().unwrap_or_default();
if named_tracker(&here) {
return;
}
if let Some(root) = configured() {
cmd.env("VISSUE_ROOT", root);
}
}
#[cfg(test)]
mod tests {
use super::*;
fn repo(at: &Path) {
std::fs::create_dir_all(at.join(".git")).unwrap();
}
#[test]
fn a_tracker_under_a_product_checkout_names_that_checkout() {
let dir = tempfile::TempDir::new().unwrap();
let product = dir.path().join("myapp");
repo(&product);
let root = product.join(".local/share/vissue/tracker");
std::fs::create_dir_all(&root).unwrap();
assert_eq!(product_repo(&root).as_deref(), Some(product.as_path()));
let said = refusal(&root, &product);
assert!(said.contains("myapp"), "{said}");
assert!(said.contains("VISSUE_ROOT"), "{said}");
}
#[test]
fn a_trackers_own_repository_is_not_a_product_checkout() {
let dir = tempfile::TempDir::new().unwrap();
let root = dir.path().join("tracker");
repo(&root);
std::fs::write(root.join("vissue.toml"), "prefix = \"Software\"\n").unwrap();
assert_eq!(product_repo(&root), None);
let under = dir.path().join("vault");
repo(&under);
let kept = under.join("tracker");
std::fs::create_dir_all(kept.join("Software")).unwrap();
std::fs::create_dir_all(under.join("Software")).unwrap();
assert_eq!(product_repo(&kept), None, "the checkout holds a tracker");
}
#[test]
fn a_tracker_in_no_checkout_at_all_is_nobody_s_files() {
let dir = tempfile::TempDir::new().unwrap();
let root = dir.path().join("share/vissue/tracker");
std::fs::create_dir_all(&root).unwrap();
assert_eq!(product_repo(&root), None);
}
#[test]
fn a_path_the_checkout_ignores_is_not_its_files() {
let dir = tempfile::TempDir::new().unwrap();
let product = dir.path().join("myapp");
std::fs::create_dir_all(&product).unwrap();
let out = crate::git_cmd()
.args(["init", "-q"])
.current_dir(&product)
.output()
.unwrap();
assert!(out.status.success(), "{out:?}");
std::fs::write(product.join(".gitignore"), ".local/\n").unwrap();
let root = product.join(".local/share/vissue/tracker");
std::fs::create_dir_all(&root).unwrap();
assert!(kept_out(&product, &root));
assert!(!kept_out(&product, &product.join("src/tracker")));
}
#[test]
fn the_root_line_is_the_top_level_one() {
assert_eq!(
root_line("root = \"/srv/tracker\"\n[routes]\n"),
Some("root = \"/srv/tracker\"")
);
assert_eq!(root_line("[routes]\nroot = \"/srv/elsewhere\"\n"), None);
assert_eq!(root_line("prefix = \"Work\"\n"), None);
}
}