wsb 0.275.7622

Workspace tools for every project under $PRJ: git across submodules, template stamping, GitHub, and the workbench
Documentation
//! Finding the roots a command works on, from any folder.
//!
//! * **Repo root** — the nearest folder, walking up, holding `.wsb/repo.json`:
//!   the repository wsb manages that the command is in. Its version, its
//!   templates, its hook. In a project with submodules it may be a submodule.
//! * **Worktree root** — the outermost git repository above the folder, when
//!   wsb manages it: the checkout of the project's main repo. Its local state
//!   ([`crate::local_state`]) and its fleet of submodules hang off it.
//!
//! ```text
//!   $PRJ/machinefabric/main-worktree/   ← worktree root (.wsb/repo.json, .wsb/project.json)
//!     capdag/                           ← repo root (.wsb/repo.json)
//!       src/                            ← where the command ran
//! ```
//!
//! Without submodules the two are the same folder.

use crate::prelude::*;
use std::path::PathBuf;

use wsb_prj::settings::RepoSettings;

/// The nearest folder at or above `start_dir` holding `.wsb/repo.json`.
pub fn find_repo_root(start_dir: &Path) -> Option<PathBuf> {
    start_dir
        .ancestors()
        .find(|dir| RepoSettings::exists(dir))
        .map(Path::to_path_buf)
}

/// The repo root, refused by name when there is none: a checkout an older
/// wsb set up is told to run `wsb migrate`, anything else `wsb init`.
pub fn require_repo_root(start_dir: &Path) -> Result<PathBuf> {
    if let Some(root) = find_repo_root(start_dir) {
        return Ok(root);
    }
    if let Some(old) = start_dir
        .ancestors()
        .find(|dir| dir.join(wsb_prj::settings::DIR).join("state.json").is_file())
    {
        anyhow::bail!(
            "{} was set up by an older wsb, whose settings this one reads from a committed \
             .wsb/repo.json. Run `wsb migrate` to bring it across",
            old.display()
        );
    }
    anyhow::bail!(
        "{} is in no repository wsb manages (no .wsb/repo.json at or above it). \
         Run `wsb init` in the repository",
        start_dir.display()
    )
}

/// The outermost git repository at or above `start_dir`, across submodule
/// boundaries; `None` outside every repository.
pub fn find_super_root(start_dir: &Path) -> Option<PathBuf> {
    let repo = gix::discover(start_dir).ok()?;
    let workdir = repo.workdir()?.to_path_buf();

    // Walk up from the discovered repository looking for one that holds it.
    // A submodule's parent has its own `.git`; a parent that discovers to a
    // folder above this one is the next level up.
    let mut current = workdir;
    while let Some(parent) = current.parent() {
        if parent == current {
            break;
        }
        if parent.join(".git").exists() {
            if let Ok(parent_repo) = gix::discover(parent) {
                if let Some(parent_workdir) = parent_repo.workdir() {
                    let parent_workdir = parent_workdir.to_path_buf();
                    if current.starts_with(&parent_workdir) {
                        current = parent_workdir;
                        continue;
                    }
                }
            }
        }
        break;
    }
    Some(current)
}

/// The worktree `start_dir` is in: its outermost repository
/// ([`find_super_root`]), when wsb manages it.
///
/// A git repository is not a worktree of a project by being one. Every
/// repository on a machine used to qualify, so a command run in `~/.claude` —
/// a repository of its own — had the daemon register it, and nine commands
/// started nine daemons for it. The one definition the store and the daemon
/// both use.
pub fn find_worktree_root(start_dir: &Path) -> Option<PathBuf> {
    find_super_root(start_dir).filter(|root| RepoSettings::exists(root))
}

/// The worktree root, refused by name when there is none.
pub fn require_worktree_root(start_dir: &Path) -> Result<PathBuf> {
    find_worktree_root(start_dir).with_context(|| {
        format!(
            "{} is in no worktree wsb manages: its outermost repository has no .wsb/repo.json. \
             Run `wsb init` there",
            start_dir.display()
        )
    })
}