Skip to main content

prospero_core/caliband/
sources.rs

1//! Workspace sources: the 1..N git checkouts a workspace root holds. Mirrors
2//! caliban's `caliban-supervisor::sources` so both sides agree on source
3//! identity (name = directory basename). See caliban #281 / ADR 0052.
4
5use std::path::Path;
6
7// The `Source` struct now lives in `prospero-types` (shared with the WASM
8// dashboard, prospero #98); re-exported here. `discover_sources` (filesystem
9// logic) stays in `prospero-core`.
10pub use prospero_types::Source;
11
12/// Is `p` a git checkout (has a `.git` entry)?
13fn is_checkout(p: &Path) -> bool {
14    p.join(".git").exists()
15}
16
17/// Enumerate the sources under `workspace_root`, matching caliban's rule:
18/// if the root is itself a checkout it is the single source; otherwise each
19/// immediate child directory that is a checkout is a source. Sorted by name.
20#[must_use]
21pub fn discover_sources(workspace_root: &Path) -> Vec<Source> {
22    let mut out = Vec::new();
23    if is_checkout(workspace_root)
24        && let Some(name) = workspace_root.file_name().and_then(|n| n.to_str())
25    {
26        out.push(Source {
27            name: name.to_string(),
28            path: workspace_root.to_path_buf(),
29        });
30        return out;
31    }
32    if let Ok(entries) = std::fs::read_dir(workspace_root) {
33        for e in entries.flatten() {
34            let path = e.path();
35            if path.is_dir()
36                && is_checkout(&path)
37                && let Some(name) = path.file_name().and_then(|n| n.to_str())
38            {
39                out.push(Source {
40                    name: name.to_string(),
41                    path,
42                });
43            }
44        }
45    }
46    out.sort_by(|a, b| a.name.cmp(&b.name));
47    out
48}
49
50#[cfg(test)]
51mod tests {
52    use super::*;
53
54    fn mk_checkout(dir: &Path) {
55        std::fs::create_dir_all(dir.join(".git")).unwrap();
56    }
57
58    #[test]
59    fn root_is_the_single_source_when_a_checkout() {
60        let d = tempfile::tempdir().unwrap();
61        mk_checkout(d.path());
62        let s = discover_sources(d.path());
63        assert_eq!(s.len(), 1);
64        assert_eq!(s[0].path, d.path());
65    }
66
67    #[test]
68    fn immediate_child_checkouts_are_sources_sorted() {
69        let d = tempfile::tempdir().unwrap();
70        mk_checkout(&d.path().join("beta"));
71        mk_checkout(&d.path().join("alpha"));
72        std::fs::create_dir_all(d.path().join("not-a-repo")).unwrap();
73        let s = discover_sources(d.path());
74        assert_eq!(
75            s.iter().map(|x| x.name.as_str()).collect::<Vec<_>>(),
76            vec!["alpha", "beta"]
77        );
78    }
79
80    #[test]
81    fn empty_when_no_checkouts() {
82        let d = tempfile::tempdir().unwrap();
83        std::fs::create_dir_all(d.path().join("plain")).unwrap();
84        assert!(discover_sources(d.path()).is_empty());
85    }
86
87    #[test]
88    fn missing_root_is_empty_not_panic() {
89        assert!(discover_sources(Path::new("/no/such/path/here")).is_empty());
90    }
91}