wsb 0.258.6441

Workspace tools for every project under $PRJ: git across submodules, template stamping, GitHub, and the workbench
Documentation
use crate::fleet::error::Result;
use crate::fleet::git::{GitOps, SubmoduleOps, SubmoduleStatus, SubmoduleTree};
use crate::fleet::output::Formatter;
use crate::prelude::*;
use rayon::prelude::*;

pub fn execute<P: AsRef<Path> + Sync>(repo_root: P, _verbose: bool) -> Result<()> {
    // Reporting status is meaningful even when nothing is left to report on —
    // the superproject's own state still matters, and an all-ignored tree is a
    // configured outcome rather than an error.
    let tree = SubmoduleTree::discover_allow_empty(&repo_root)?;

    let max_depth = tree
        .top_down()
        .iter()
        .map(|node| node.depth + 1)
        .max()
        .unwrap_or(0);
    let ignored = tree.ignored().len();
    Formatter::print_info(&format!(
        "Superproject + {} submodule(s) across {} level(s){}",
        tree.len(),
        max_depth,
        if ignored == 0 {
            String::new()
        } else {
            format!(" ({ignored} ignored)")
        }
    ));

    // Every generated file that stands exactly as a render would write it,
    // computed ONCE for the whole fleet. Under `wsb local on` this workspace
    // carries one in nearly every repository, and reporting them as changes is
    // what made this command unreadable: seventy-five rendered manifests and
    // version files burying the handful of paths somebody edited.
    //
    // A property of the workspace, so it is produced here and handed down,
    // rather than re-derived per repository — which would render the fleet once
    // per submodule.
    let rendered = crate::state::discard::rendered_as_written(
        repo_root.as_ref(),
        &crate::state::RunCache::new(),
    )
    .map_err(|e| crate::fleet::error::SuperGitError::Other(e.to_string()))?;

    // Get status for superproject
    let repo = GitOps::open_repo(&repo_root)?;
    let is_detached = GitOps::is_detached_head(&repo)?;
    let current_branch = if is_detached {
        None
    } else {
        Some(GitOps::current_branch(&repo)?)
    };
    // A single status walk answers both questions; asking separately walks the
    // whole worktree twice.
    let (changed, moved_pointers) = GitOps::uncommitted_files_and_pointers(&repo)?;
    let (uncommitted_files, rendered_files) =
        crate::fleet::git::split_rendered(repo_root.as_ref(), changed, &rendered);
    let is_clean = uncommitted_files.is_empty();
    let (ahead, behind) = if !is_detached {
        GitOps::get_ahead_behind(&repo).unwrap_or((0, 0))
    } else {
        (0, 0)
    };

    let super_status = SubmoduleStatus {
        name: "[SUPERPROJECT]".to_string(),
        path: ".".to_string(),
        current_branch,
        is_clean,
        is_detached,
        ahead,
        behind,
        uncommitted_files,
        rendered_files,
        moved_pointers,
        exists: true,
        depth: 0,
    };

    // Status for every submodule at every depth, in parallel. Discovery order is
    // preserved so children stay under their parent in the output.
    let mut statuses: Vec<_> = tree
        .top_down()
        .par_iter()
        .map(|node| SubmoduleOps::get_status(node, &rendered))
        .collect::<Result<Vec<_>>>()?;

    // Prepend superproject status
    statuses.insert(0, super_status);

    Formatter::print_status(&statuses);

    Ok(())
}