wsb 0.199.4355

Workspace - A tool suite for file operations, version management, and development workflow automation
Documentation
//! Workspace resolution — finding the right roots from any CWD.
//!
//! Three tiers of path resolution:
//!
//! 1. **CWD** — the directory where the user invoked `wsb`.
//!    Used by: refactor (file operations work relative to CWD).
//!
//! 2. **Project root** — nearest ancestor directory containing `.wsb/state.json`.
//!    Used by: update (reads StateConfig, WorkspaceState), template commands,
//!    init (creates `.wsb/` here), state config.
//!
//! 3. **Super root** — the most senior git repository root found by walking up
//!    from the project root. When there are no submodules, super root == project root.
//!    Used by: serve (DB location), session sync (the wsb.db lives here).
//!
//! In the common case (no submodules), project root == super root.
//! In a submodule scenario:
//!
//!   ~/projects/my-org/           ← super root (.git, .wsb/wsb.db)
//!     my-submodule/              ← project root (.git, .wsb/state.json)
//!       src/                     ← CWD
//!
//! Commands resolve what they need:
//!
//! | Command      | CWD | Project root | Super root |
//! |--------------|-----|-------------|------------|
//! | init         | ✓   | (creates)   | —          |
//! | update       | —   | ✓           | —          |
//! | serve        | —   | —           | ✓+         |
//! | refactor        | ✓   | —           | —          |
//! | template     | —   | ✓           | —          |
//! | db operations| —   | —           | ✓          |

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

/// Walk up from `start_dir` looking for `.wsb/state.json`.
/// Returns the first ancestor (including `start_dir` itself) that contains it,
/// or `None` if no workspace is found.
pub fn find_project_root(start_dir: &Path) -> Option<PathBuf> {
    let mut dir = start_dir;
    loop {
        if dir.join(".wsb").join("state.json").exists() {
            return Some(dir.to_path_buf());
        }
        dir = dir.parent()?;
    }
}

/// Walk up from `start_dir` past all git repositories to find the most senior
/// (top-level) git repo root. This traverses submodule boundaries.
///
/// If `start_dir` is not inside any git repo, returns `None`.
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 repo's workdir looking for a parent git repo.
    // If we find one, that parent is the super root — recurse upward.
    // If not, this repo is the topmost.
    let mut current = workdir;
    while let Some(parent) = current.parent() {
        // Skip the root dir (no point checking /)
        if parent == current {
            break;
        }
        // Check if parent directory is a different git repo.
        // Use .git dir existence as a quick check — submodule parents
        // have their own .git as a file pointing to the super repo.
        if parent.join(".git").exists() {
            // Verify it's a real separate repo by trying to discover it
            if let Ok(parent_repo) = gix::discover(parent) {
                if let Some(parent_workdir) = parent_repo.workdir() {
                    let parent_workdir = parent_workdir.to_path_buf();
                    // Only continue if the parent is truly above us
                    if current.starts_with(&parent_workdir) {
                        current = parent_workdir;
                        continue;
                    }
                }
            }
        }
        break;
    }
    Some(current)
}

/// Find the super root, falling back to the project root if no git repo is found.
/// This ensures we always have a valid DB location.
pub fn find_super_root_or_project(start_dir: &Path) -> PathBuf {
    find_super_root(start_dir)
        .or_else(|| find_project_root(start_dir))
        .unwrap_or_else(|| start_dir.to_path_buf())
}

/// Find the project root or fail with a descriptive error.
pub fn require_project_root(start_dir: &Path) -> Result<PathBuf> {
    find_project_root(start_dir).with_context(|| {
        format!(
            "Not in a wsb workspace (no .wsb/state.json found walking up from {}). \
             Run 'wsb init' to initialize one.",
            start_dir.display()
        )
    })
}