repoverse 0.1.1

Multi-repo workspace tool: keep many git repos in sync and roll changes up across dependency boundaries
//! Workspace = root dir + config + lock. Project discovery & target resolution.

use crate::config::{Config, Project};
use crate::lock::Lock;
use anyhow::{bail, Result};
use std::path::{Path, PathBuf};

pub struct Workspace {
    pub root: PathBuf,
    pub config: Config,
    #[allow(dead_code)]
    pub config_path: PathBuf,
    pub lock_path: PathBuf,
}

impl Workspace {
    /// Discover the workspace from the current dir upward.
    pub fn discover() -> Result<Workspace> {
        let cwd = std::env::current_dir()?;
        Self::discover_from(&cwd)
    }

    pub fn discover_from(start: &Path) -> Result<Workspace> {
        let config_path = Config::discover(start).ok_or_else(|| {
            anyhow::anyhow!(
                "no {} found (run `rv adopt` or `rv init`)",
                crate::config::CONFIG_FILE
            )
        })?;
        let root = config_path.parent().unwrap().to_path_buf();
        let config = Config::load(&config_path)?;
        let lock_path = root.join(crate::lock::LOCK_FILE);
        Ok(Workspace {
            root,
            config,
            config_path,
            lock_path,
        })
    }

    pub fn lock(&self) -> Result<Lock> {
        Lock::load_or_default(&self.lock_path)
    }

    pub fn project_dir(&self, p: &Project) -> PathBuf {
        if p.path == "." {
            self.root.clone()
        } else {
            self.root.join(&p.path)
        }
    }

    /// Resolve user targets (paths, names, or "all"/empty) to projects.
    pub fn resolve_targets<'a>(&'a self, targets: &[String]) -> Result<Vec<&'a Project>> {
        if targets.is_empty() || targets == ["all"] {
            return Ok(self.config.projects.iter().collect());
        }
        let mut out = Vec::new();
        for t in targets {
            let matches: Vec<&Project> = self
                .config
                .projects
                .iter()
                .filter(|p| {
                    &p.path == t || &p.name == t || p.name.rsplit('/').next() == Some(t.as_str())
                })
                .collect();
            match matches.as_slice() {
                [p] => out.push(*p),
                [] => bail!("unknown target `{t}`"),
                _ => bail!("ambiguous target `{t}`; use a full path or owner/repo name"),
            }
        }
        Ok(out)
    }
}