Skip to main content

ytcli/config/
paths.rs

1//! Per-OS locations and the upward search for a repository pin.
2
3use std::path::{Path, PathBuf};
4
5use etcetera::BaseStrategy;
6
7use crate::config::ProjectPin;
8
9/// Name of the committed, secret-free per-repository pin file.
10pub const PROJECT_FILE: &str = ".tracker.toml";
11
12#[derive(Debug, thiserror::Error)]
13pub enum PathsError {
14    #[error("no home directory found")]
15    NoHome(#[from] etcetera::HomeDirError),
16}
17
18/// `~/.config/ytcli/config.toml` on Linux, `~/Library/Application Support/...`
19/// on macOS, `%APPDATA%\...` on Windows.
20pub fn config_file() -> Result<PathBuf, PathsError> {
21    Ok(etcetera::choose_base_strategy()?
22        .config_dir()
23        .join("ytcli")
24        .join("config.toml"))
25}
26
27/// Walk up from `start` looking for [`PROJECT_FILE`], the way git looks for `.git`.
28///
29/// A malformed pin is ignored rather than fatal: a broken file in some parent
30/// directory must not make the tool unusable everywhere below it.
31#[must_use]
32pub fn find_project_pin(start: &Path) -> Option<(PathBuf, ProjectPin)> {
33    for dir in start.ancestors() {
34        let candidate = dir.join(PROJECT_FILE);
35        if !candidate.is_file() {
36            continue;
37        }
38        let Ok(text) = std::fs::read_to_string(&candidate) else {
39            continue;
40        };
41        match toml_from_str(&text) {
42            Some(pin) => return Some((candidate, pin)),
43            None => {
44                tracing::warn!(path = %candidate.display(), "ignoring malformed .tracker.toml");
45            }
46        }
47    }
48    None
49}
50
51fn toml_from_str(text: &str) -> Option<ProjectPin> {
52    use figment::Figment;
53    use figment::providers::{Format, Toml};
54
55    Figment::new().merge(Toml::string(text)).extract().ok()
56}
57
58#[cfg(test)]
59#[allow(clippy::expect_used)]
60mod tests {
61    use super::*;
62
63    #[test]
64    fn finds_pin_in_parent_directory() {
65        let root = tempfile::tempdir().expect("tempdir");
66        std::fs::write(
67            root.path().join(PROJECT_FILE),
68            "profile = \"work\"\nqueue = \"PROJ\"\n",
69        )
70        .expect("write pin");
71        let nested = root.path().join("a").join("b");
72        std::fs::create_dir_all(&nested).expect("create nested");
73
74        let (path, pin) = find_project_pin(&nested).expect("pin found");
75
76        assert_eq!(path, root.path().join(PROJECT_FILE));
77        assert_eq!(pin.profile.as_deref(), Some("work"));
78        assert_eq!(pin.queue.as_deref(), Some("PROJ"));
79    }
80
81    #[test]
82    fn malformed_pin_is_ignored_not_fatal() {
83        let root = tempfile::tempdir().expect("tempdir");
84        std::fs::write(root.path().join(PROJECT_FILE), "profile = [[[").expect("write pin");
85
86        assert!(find_project_pin(root.path()).is_none());
87    }
88}