wsb 0.275.7622

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

pub fn execute<P: AsRef<Path> + Sync>(
    repo_root: P,
    all: bool,
    force_all: bool,
    dry_run: bool,
    verbose: bool,
) -> Result<()> {
    let tree = SubmoduleTree::discover(&repo_root)?;

    Formatter::print_info(&format!(
        "Fetching updates for superproject + {} submodule(s){}",
        tree.len(),
        if all { " (all remotes)" } else { "" }
    ));

    if dry_run {
        Formatter::print_warning("DRY RUN: No changes will be made");
    }

    // Fetching touches no working tree, so every repository in the tree — at any
    // depth — can run concurrently regardless of parent/child relationships.
    let results: Vec<_> = tree
        .top_down()
        .par_iter()
        .map(|node| {
            let label = node.display_path.clone();

            if !node.initialized {
                return (label, Err("not initialized".to_string()));
            }

            let repo = match GitOps::open_repo(&node.abs_path) {
                Ok(repo) => repo,
                Err(e) => return (label, Err(e.to_string())),
            };

            if !force_all {
                match git_backend::has_fetchable_updates(&repo, all) {
                    Ok(true) => {}
                    Ok(false) => return (label, Ok(false)),
                    Err(e) => return (label, Err(e.to_string())),
                }
            }

            if dry_run {
                return (label, Ok(true));
            }

            match SubmoduleOps::fetch(node, all) {
                Ok(_) => (label, Ok(true)),
                Err(e) => (label, Err(e.to_string())),
            }
        })
        .collect();

    let mut successful = 0;
    let mut failed = 0;
    let mut skipped = 0;

    if verbose {
        Formatter::print_submodule_header("[SUPERPROJECT]");
    }
    match fetch_repository(repo_root.as_ref(), all, force_all, dry_run) {
        Ok(true) => {
            if dry_run {
                Formatter::print_info("Would fetch superproject");
            } else {
                Formatter::print_success("Fetched superproject");
            }
            successful += 1;
        }
        Ok(false) => {
            if verbose {
                Formatter::print_info("No fetchable remote updates in superproject");
            }
            skipped += 1;
        }
        Err(e) => {
            Formatter::print_error(&format!("Failed to fetch superproject: {}", e));
            failed += 1;
        }
    }

    for (name, result) in results {
        match result {
            Ok(true) => {
                if verbose {
                    if dry_run {
                        Formatter::print_info(&format!("Would fetch '{}'", name));
                    } else {
                        Formatter::print_success(&format!("Fetched '{}'", name));
                    }
                }
                successful += 1;
            }
            Ok(false) => {
                if verbose {
                    Formatter::print_info(&format!("No fetchable remote updates in '{}'", name));
                }
                skipped += 1;
            }
            Err(e) => {
                if e == "not initialized" {
                    if verbose {
                        Formatter::print_warning(&format!("'{}' not initialized, skipping", name));
                    }
                    skipped += 1;
                } else {
                    Formatter::print_error(&format!("Failed to fetch '{}': {}", name, e));
                    failed += 1;
                }
            }
        }
    }

    Formatter::print_summary(successful, failed, skipped);

    if failed > 0 {
        return Err(SuperGitError::Other(format!(
            "{} repository/submodule(s) failed to fetch",
            failed
        )));
    }

    Ok(())
}

fn fetch_repository(
    repo_path: &Path,
    all: bool,
    force_all: bool,
    dry_run: bool,
) -> std::result::Result<bool, String> {
    let repo = git_backend::open_repo(repo_path).map_err(|err| err.to_string())?;

    if !force_all {
        match git_backend::has_fetchable_updates(&repo, all) {
            Ok(true) => {}
            Ok(false) => return Ok(false),
            Err(e) => return Err(e.to_string()),
        }
    }

    if dry_run {
        return Ok(true);
    }

    if all {
        git_backend::fetch_all(&repo).map_err(|err| err.to_string())?;
    } else {
        git_backend::fetch(&repo, Some("origin")).map_err(|err| err.to_string())?;
    }

    Ok(true)
}