Skip to main content

hm_util/
dirs.rs

1//! Harmont-specific directory resolution.
2//!
3//! Every directory accessor in this module returns an `hm`-namespaced path
4//! under an XDG-correct root: configuration in `~/.config/hm/`, regenerable
5//! cache in `~/.cache/hm/`. Raw platform primitives (`home_dir`, `config_dir`,
6//! `cache_dir`) live in `os::dirs` and are **not** re-exported — callers
7//! outside `hm-util` should never need them.
8
9#![allow(clippy::must_use_candidate)]
10
11use std::path::PathBuf;
12
13use crate::os::dirs as platform;
14
15/// `~/.config/hm/` — user config root (`config.toml`, `credentials.toml`).
16pub fn hm_config_dir() -> Option<PathBuf> {
17    platform::config_dir().map(|c| c.join("hm"))
18}
19
20/// `~/.cache/hm/` — local build cache root (regenerable).
21pub fn hm_cache_dir() -> Option<PathBuf> {
22    platform::cache_dir().map(|c| c.join("hm"))
23}
24
25/// `~/.cache/hm/workspaces/` — COW workspace cache root.
26pub fn hm_workspace_cache_dir() -> Option<PathBuf> {
27    hm_cache_dir().map(|c| c.join("workspaces"))
28}
29
30/// Walk up from `start` looking for a directory containing `.hm/`.
31/// Returns the project root (the directory *containing* `.hm/`),
32/// or `None` if the filesystem root is reached without finding one.
33pub fn find_project_root(start: &std::path::Path) -> Option<PathBuf> {
34    let mut current = start;
35    loop {
36        if current.join(".hm").is_dir() {
37            return Some(current.to_path_buf());
38        }
39        current = current.parent()?;
40    }
41}
42
43#[cfg(test)]
44#[allow(clippy::unwrap_used)]
45mod tests {
46    use super::*;
47
48    #[test]
49    fn hm_config_dir_under_config() {
50        let p = hm_config_dir().unwrap();
51        assert!(p.ends_with("hm"), "expected path ending in 'hm', got {p:?}");
52        let parent = p.parent().unwrap();
53        assert!(
54            parent.ends_with(".config") || parent.ends_with("AppData/Roaming"),
55            "unexpected parent: {parent:?}"
56        );
57    }
58
59    #[test]
60    fn hm_cache_dir_under_cache() {
61        let p = hm_cache_dir().unwrap();
62        assert!(p.ends_with("hm"), "expected path ending in 'hm', got {p:?}");
63    }
64
65    #[test]
66    fn hm_workspace_cache_dir_resolves() {
67        let p = hm_workspace_cache_dir().unwrap();
68        assert!(p.ends_with("hm/workspaces"), "got {p:?}");
69    }
70
71    #[test]
72    fn find_project_root_at_current_dir() {
73        let tmp = tempfile::tempdir().unwrap();
74        std::fs::create_dir(tmp.path().join(".hm")).unwrap();
75        let found = find_project_root(tmp.path());
76        assert_eq!(found, Some(tmp.path().to_path_buf()));
77    }
78
79    #[test]
80    fn find_project_root_walks_up() {
81        let tmp = tempfile::tempdir().unwrap();
82        std::fs::create_dir(tmp.path().join(".hm")).unwrap();
83        let nested = tmp.path().join("src").join("deep");
84        std::fs::create_dir_all(&nested).unwrap();
85        let found = find_project_root(&nested);
86        assert_eq!(found, Some(tmp.path().to_path_buf()));
87    }
88
89    #[test]
90    fn find_project_root_returns_none_when_missing() {
91        let tmp = tempfile::tempdir().unwrap();
92        let found = find_project_root(tmp.path());
93        assert_eq!(found, None);
94    }
95}