wsb 0.275.7622

Workspace tools for every project under $PRJ: git across submodules, template stamping, GitHub, and the workbench
Documentation
//! Where wsb keeps what it knows about a worktree and never commits.
//!
//! A worktree's `.wsb/` is committed, all of it ([`wsb_prj::settings`]). What
//! wsb keeps beside it for itself lives elsewhere: its test catalogue and
//! workbench layout in the workshop's one database ([`crate::store`]), keyed
//! by [`LocalState::key`], and its files — logs, the lock its writers take —
//! in a folder under `$PRJ/.state`:
//!
//! * a worktree of a workspace in the workshop:
//!   `$PRJ/.state/workspaces/<project>/worktrees/<worktree>/`, kept by name,
//!   so a worktree removed and made again under the same name finds it;
//! * a checkout outside the workshop, or on a machine with no `$PRJ`:
//!   `<machine dir>/outside/<key>/`, keyed by the checkout's resolved path,
//!   because such a path only means anything on the machine it is on.

use std::path::{Path, PathBuf};

use anyhow::{bail, Context, Result};

/// The folder of a machine's state that holds checkouts outside the workshop.
pub const OUTSIDE_DIR: &str = "outside";

/// One worktree's local state.
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct LocalState {
    dir: PathBuf,
    key: String,
}

impl LocalState {
    /// The local state of the worktree checked out at `worktree_root`.
    ///
    /// A path in a workspace but in none of its worktrees is refused: it is
    /// no worktree, and filing state for it would invent one.
    pub fn of(worktree_root: &Path) -> Result<Self> {
        let (dir, key) = match wsb_prj::Workshop::from_env() {
            Ok(workshop) => match workshop.locate(worktree_root) {
                Ok(wsb_prj::Location {
                    workspace,
                    worktree: Some(worktree),
                }) => (workspace.worktree_state_dir(&worktree), format!("{}/{worktree}", workspace.name())),
                Ok(wsb_prj::Location { workspace, worktree: None }) => bail!(
                    "{} is in the workspace of {} but in none of its worktrees, so it has no local state",
                    worktree_root.display(),
                    workspace.name()
                ),
                Err(wsb_prj::WorkshopError::Outside { .. }) => outside(worktree_root)?,
                Err(other) => return Err(other.into()),
            },
            Err(wsb_prj::WorkshopError::NotSet) => outside(worktree_root)?,
            Err(other) => return Err(other.into()),
        };
        Ok(Self { dir, key })
    }

    /// What the workshop's database names this worktree by:
    /// `<workspace>/<worktree>` for one of the workshop's,
    /// `outside/<digest>` for a checkout elsewhere.
    pub fn key(&self) -> &str {
        &self.key
    }

    /// The folder itself.
    pub fn dir(&self) -> &Path {
        &self.dir
    }

    /// The logs of what ran in the worktree.
    pub fn logs(&self) -> PathBuf {
        self.dir.join("logs")
    }

    /// The build folder the stamp uses when `stamp.json` names none.
    pub fn build(&self) -> PathBuf {
        self.dir.join("build")
    }

    /// The lock wsb's writers of the worktree take
    /// ([`crate::worktree_lock`]).
    pub fn lock(&self) -> PathBuf {
        self.dir.join("worktree.lock")
    }

    /// The folder each process writing the worktree registers in.
    pub fn writers(&self) -> PathBuf {
        self.dir.join("worktree.writers")
    }
}

/// `<machine dir>/outside/<digest>` for a checkout outside the workshop, and
/// its key.
fn outside(checkout: &Path) -> Result<(PathBuf, String)> {
    let resolved = crate::platform::canonicalize(checkout)
        .with_context(|| format!("resolving {}", checkout.display()))?;
    let machine = crate::daemon::machine_dir().map_err(anyhow::Error::msg)?;
    let digest = wsb_prj::short_digest(&resolved.to_string_lossy());
    Ok((machine.join(OUTSIDE_DIR).join(&digest), format!("{OUTSIDE_DIR}/{digest}")))
}

#[cfg(test)]
mod tests;