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, SubmoduleTree};
use crate::fleet::output::Formatter;
use crate::git_backend::{self, PushOutcome};
use crate::prelude::*;

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

    Formatter::print_info(&format!(
        "Pushing changes in superproject + {} submodule(s){}",
        tree.len(),
        if force_with_lease {
            " (with --force-with-lease)"
        } else {
            ""
        }
    ));

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

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

    // Deepest first, superproject last: a gitlink is only meaningful to a remote
    // once the commit it points at has been published by the child.
    for node in tree.bottom_up() {
        if verbose {
            Formatter::print_submodule_header(node.label());
        }

        if !node.initialized {
            Formatter::print_warning(&format!(
                "Submodule '{}' not initialized, skipping",
                node.label()
            ));
            skipped += 1;
            continue;
        }

        let repo = match GitOps::open_repo(&node.abs_path) {
            Ok(r) => r,
            Err(e) => {
                Formatter::print_error(&format!("Failed to open '{}': {}", node.label(), e));
                failed += 1;
                continue;
            }
        };

        let is_detached = match GitOps::is_detached_head(&repo) {
            Ok(d) => d,
            Err(e) => {
                Formatter::print_error(&format!(
                    "Failed to check HEAD state of '{}': {}",
                    node.label(),
                    e
                ));
                failed += 1;
                continue;
            }
        };

        if is_detached {
            Formatter::print_warning(&format!(
                "Submodule '{}' is in detached HEAD state, skipping",
                node.label()
            ));
            skipped += 1;
            continue;
        }

        // Fetch, then decide. A branch that has DIVERGED has outgoing commits
        // and cannot push them; reading the old boolean's `false` as "nothing
        // to send" skipped exactly the repository that needed attention.
        let state = match git_backend::remote_state(&repo, Some("origin")) {
            Ok(value) => value,
            Err(e) => {
                Formatter::print_error(&format!(
                    "Failed to determine the state of '{}': {}",
                    node.label(),
                    e
                ));
                failed += 1;
                continue;
            }
        };

        if let git_backend::RemoteState::Diverged {
            incoming,
            outgoing,
        } = state
        {
            Formatter::print_error(&format!(
                "'{}' has diverged: {outgoing} local commit(s) and {incoming} incoming — \
                 pull (or `wsb pull --rebase`) before pushing",
                node.label()
            ));
            failed += 1;
            continue;
        }

        if !force_all && !state.has_outgoing() {
            if verbose {
                Formatter::print_info(&format!("'{}' {}", node.label(), state.describe()));
            }
            skipped += 1;
            continue;
        }

        if dry_run {
            Formatter::print_info("Would push changes");
            successful += 1;
            continue;
        }

        match git_backend::push_current_branch(&repo, Some("origin"), true, force_with_lease) {
            Ok(PushOutcome::Updated) => {
                Formatter::print_success(&format!("Pushed changes in '{}'", node.label()));
                successful += 1;
            }
            Ok(PushOutcome::UpToDate) => {
                Formatter::print_info(&format!("'{}' is up to date", node.label()));
                successful += 1;
            }
            Err(e) => {
                Formatter::print_error(&format!("Failed to push in '{}': {}", node.label(), e));
                failed += 1;
            }
        }
    }

    // Push superproject after submodules
    if !dry_run {
        if verbose {
            Formatter::print_submodule_header("[SUPERPROJECT]");
        }

        let repo = git_backend::open_repo(repo_root.as_ref())?;
        let state = git_backend::remote_state(&repo, Some("origin"))?;
        if let git_backend::RemoteState::Diverged {
            incoming,
            outgoing,
        } = state
        {
            Formatter::print_error(&format!(
                "Superproject has diverged: {outgoing} local commit(s) and {incoming} \
                 incoming — pull (or `wsb pull --rebase`) before pushing"
            ));
            failed += 1;
            Formatter::print_summary(successful, failed, skipped);
        } else if !force_all && !state.has_outgoing() {
            Formatter::print_info(&format!("Superproject {}, skipping push", state.describe()));
            skipped += 1;
            Formatter::print_summary(successful, failed, skipped);
        } else {
            match git_backend::push_current_branch(&repo, Some("origin"), true, force_with_lease) {
                Ok(PushOutcome::Updated) => {
                    Formatter::print_success("Pushed superproject");
                    successful += 1;
                }
                Ok(PushOutcome::UpToDate) => {
                    Formatter::print_info("Superproject is up to date");
                    successful += 1;
                }
                Err(err) => {
                    Formatter::print_error(&format!("Failed to push superproject: {}", err));
                    failed += 1;
                }
            }

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

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

    Ok(())
}