wsb 0.258.6441

Workspace tools for every project under $PRJ: git across submodules, template stamping, GitHub, and the workbench
Documentation
use crate::fleet::config::GitModules;
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::*;

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

    Formatter::print_info(&format!(
        "Creating branch '{}' in superproject + {} submodule(s){}",
        branch,
        tree.len(),
        if let Some(from) = from_branch {
            format!(" from '{}'", from)
        } else {
            String::new()
        }
    ));

    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;
    // A submodule's `branch` field lives in the `.gitmodules` of the repository
    // that declares it, which for a nested submodule is not the fleet root.
    let mut branched: Vec<(PathBuf, PathBuf)> = Vec::new();

    // Create branch in superproject first only when it has worktree changes by default.
    if verbose {
        Formatter::print_submodule_header("[SUPERPROJECT]");
    }

    let super_repo = GitOps::open_repo(&repo_root)?;
    let super_is_clean = GitOps::is_clean(&super_repo)?;
    let super_branch_exists = GitOps::branch_exists(&super_repo, branch)?;

    if super_is_clean && !force_all {
        Formatter::print_info("No changes in superproject, skipping branch creation");
        skipped += 1;
    } else if super_branch_exists {
        Formatter::print_warning(&format!(
            "Branch '{}' already exists in superproject, skipping creation",
            branch
        ));
        skipped += 1;
    } else {
        if !dry_run {
            if let Err(err) = git_backend::create_branch(&super_repo, branch, from_branch) {
                Formatter::print_error(&format!(
                    "Failed to create branch '{}' in superproject: {}",
                    branch, err
                ));
                return Err(SuperGitError::Other(
                    "Superproject branch creation failed".to_string(),
                ));
            }

            Formatter::print_success(&format!("Created branch '{}' in superproject", branch));
            successful += 1;

            if push {
                match git_backend::push_current_branch(&super_repo, Some("origin"), true, false) {
                    Ok(_) => {
                        Formatter::print_success(&format!(
                            "Pushed branch '{}' in superproject",
                            branch
                        ));
                    }
                    Err(err) => {
                        Formatter::print_error(&format!(
                            "Failed to push '{}' in superproject: {}",
                            branch, err
                        ));
                        failed += 1;
                    }
                }
            }
        } else {
            Formatter::print_info(&format!("Would create branch '{}'", branch));
            successful += 1;
        }
    }

    for node in tree.top_down() {
        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_clean = match GitOps::is_clean(&repo) {
            Ok(c) => c,
            Err(e) => {
                Formatter::print_error(&format!(
                    "Failed to check status of '{}': {}",
                    node.label(),
                    e
                ));
                failed += 1;
                continue;
            }
        };

        if is_clean && !force_all {
            if verbose {
                Formatter::print_info(&format!("No changes in '{}'", node.label()));
            }
            skipped += 1;
            continue;
        }

        // Check if branch already exists
        let branch_exists = match GitOps::branch_exists(&repo, branch) {
            Ok(exists) => exists,
            Err(e) => {
                Formatter::print_error(&format!(
                    "Failed to check branch in '{}': {}",
                    node.label(),
                    e
                ));
                failed += 1;
                continue;
            }
        };

        if branch_exists {
            Formatter::print_warning(&format!(
                "Branch '{}' already exists in '{}', skipping",
                branch,
                node.label()
            ));
            skipped += 1;
            continue;
        }

        if dry_run {
            Formatter::print_info(&format!("Would create branch '{}'", branch));
            successful += 1;
            continue;
        }

        // Create branch
        match SubmoduleOps::create_branch(node, branch, from_branch) {
            Ok(_) => {
                Formatter::print_success(&format!(
                    "Created branch '{}' in '{}'",
                    branch,
                    node.label()
                ));
                successful += 1;
                branched.push((node.parent_path.clone(), node.rel_path.clone()));

                // Push if requested
                if push {
                    match SubmoduleOps::push(node, false) {
                        Ok(_) => {
                            Formatter::print_success(&format!(
                                "Pushed '{}' in '{}'",
                                branch,
                                node.label()
                            ));
                        }
                        Err(e) => {
                            Formatter::print_error(&format!(
                                "Failed to push '{}' in '{}': {}",
                                branch,
                                node.label(),
                                e
                            ));
                            failed += 1;
                        }
                    }
                }
            }
            Err(e) => {
                Formatter::print_error(&format!(
                    "Failed to create branch '{}' in '{}': {}",
                    branch,
                    node.label(),
                    e
                ));
                failed += 1;
            }
        }
    }

    if !dry_run {
        Formatter::print_summary(successful, failed, skipped);

        // Update each declaring repository's .gitmodules to track the new branch
        if !branched.is_empty() {
            Formatter::print_info("Updating .gitmodules branch fields...");
            for (parent_path, rel_path) in &branched {
                let rel_path = rel_path.to_str().ok_or_else(|| {
                    SuperGitError::Other(format!(
                        "Submodule path '{}' is not valid UTF-8 and cannot be written to .gitmodules",
                        rel_path.display()
                    ))
                })?;
                GitModules::update_branch(parent_path, rel_path, branch)?;
            }
            Formatter::print_success("Updated .gitmodules");
        }
    }

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

    Ok(())
}