nornir 0.4.3

Companion to cargo: dependency tracking, release gating, deploy, benchmarks, and documentation assembly. Project-agnostic.
Documentation
//! Resolve every repo in a [`WorkspaceDescriptor`] to a concrete local
//! filesystem path that the dep-graph builder and release pipeline can
//! consume uniformly.
//!
//! - `path = "…"` → resolved relative to the descriptor's parent dir.
//! - `git  = "…"` → mapped to a deterministic cache path under
//!   `$NORNIR_CACHE_DIR` (default `~/.cache/nornir/workspaces/<ws>/<repo>`).
//!   If the cache path doesn't exist, returns an error containing the
//!   exact `git clone` command the operator should run. Actual cloning
//!   over the network is intentionally **not** done here yet — gix
//!   requires extra features for HTTPS transport and we don't want to
//!   pull reqwest/curl into nornir's tree just for the bootstrap path.
//!   `nornir workspace fetch` will fill this in once that decision lands.
//!
//! Pure Rust, sync.

use std::collections::BTreeMap;
use std::path::PathBuf;

use anyhow::{Result, anyhow};

use crate::workspace::descriptor::{RepoSource, WorkspaceDescriptor};

/// Deterministic cache dir for one (workspace, repo) git source.
///
/// Respects `$NORNIR_CACHE_DIR`; falls back to `$XDG_CACHE_HOME` then
/// `$HOME/.cache`. Final layout: `…/nornir/workspaces/<ws>/<repo>`.
pub fn git_cache_dir(workspace_name: &str, repo_name: &str) -> Result<PathBuf> {
    let base = if let Some(d) = std::env::var_os("NORNIR_CACHE_DIR") {
        PathBuf::from(d)
    } else if let Some(d) = std::env::var_os("XDG_CACHE_HOME") {
        PathBuf::from(d).join("nornir")
    } else if let Some(home) = std::env::var_os("HOME") {
        PathBuf::from(home).join(".cache").join("nornir")
    } else {
        return Err(anyhow!("no $HOME and no $NORNIR_CACHE_DIR — can't pick cache dir"));
    };
    Ok(base.join("workspaces").join(workspace_name).join(repo_name))
}

/// Resolve every repo to a local path. For `git:` sources, errors out
/// (with a clone hint) if the cache path is missing.
pub fn resolve_sources(desc: &WorkspaceDescriptor) -> Result<BTreeMap<String, PathBuf>> {
    let mut out = BTreeMap::new();
    for (name, src) in desc.sources()? {
        let path = match src {
            RepoSource::Path(p) => p,
            RepoSource::Git { url, branch } => {
                let cache = git_cache_dir(&desc.workspace.name, &name)?;
                if !cache.join(".git").exists() {
                    let br = branch
                        .as_deref()
                        .map(|b| format!(" --branch {b}"))
                        .unwrap_or_default();
                    return Err(anyhow!(
                        "repo `{name}` is git-sourced and not yet in cache.\n\
                         Bootstrap once with:\n\
                            mkdir -p {parent}\n\
                            git clone{br} {url} {cache}\n\
                         (network clone via gix is not yet wired; \
                         once it is, `nornir workspace fetch {name}` will do this for you.)",
                        parent = cache.parent().unwrap().display(),
                        cache = cache.display(),
                    ));
                }
                cache
            }
        };
        out.insert(name, path);
    }
    Ok(out)
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn cache_dir_respects_explicit_env() {
        // SAFETY: single-threaded test; we restore env at end.
        let prev = std::env::var_os("NORNIR_CACHE_DIR");
        unsafe { std::env::set_var("NORNIR_CACHE_DIR", "/tmp/nornir-cache-test"); }
        let d = git_cache_dir("ws1", "repo1").unwrap();
        assert_eq!(d, PathBuf::from("/tmp/nornir-cache-test/workspaces/ws1/repo1"));
        unsafe {
            match prev {
                Some(v) => std::env::set_var("NORNIR_CACHE_DIR", v),
                None => std::env::remove_var("NORNIR_CACHE_DIR"),
            }
        }
    }
}