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 dialoguer::Confirm;

#[derive(Debug, Clone, Copy)]
struct BranchAvailability {
    local: bool,
    remote_tracking: bool,
    remote_advertised: bool,
}

impl BranchAvailability {
    fn available(self) -> bool {
        self.local || self.remote_tracking || self.remote_advertised
    }

    fn needs_fetch(self) -> bool {
        !self.local && !self.remote_tracking && self.remote_advertised
    }
}

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

    Formatter::print_info(&format!(
        "Checking out '{}' in superproject + {} submodule(s){}",
        branch,
        tree.len(),
        if create { " (creating if needed)" } 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;

    // Checkout in superproject first
    if verbose {
        Formatter::print_submodule_header("[SUPERPROJECT]");
    }

    let super_repo = GitOps::open_repo(&repo_root)?;
    let super_branch = branch_availability(&super_repo, branch, create)?;

    if !super_branch.available() && !create && ignore_missing {
        Formatter::print_info(&format!(
            "Branch '{}' does not exist in superproject, skipping",
            branch
        ));
        skipped += 1;
    } else if !super_branch.available() && !create {
        Formatter::print_error(&format!(
            "Branch '{}' does not exist in superproject. Use --create to create it.",
            branch
        ));
        return Err(SuperGitError::Other(
            "Branch does not exist in superproject".to_string(),
        ));
    } else {
        let super_uncommitted = GitOps::get_uncommitted_files(&super_repo)?;

        if !super_uncommitted.is_empty() && !dry_run {
            Formatter::print_warning(&format!(
                "Superproject has uncommitted changes: {}",
                GitOps::describe_uncommitted(&super_uncommitted)
            ));
            let should_continue = Confirm::new()
                .with_prompt("Do you want to continue? (changes may be lost)")
                .default(false)
                .interact()
                .unwrap_or(false);

            if !should_continue {
                return Err(SuperGitError::Cancelled);
            }
        }

        if !dry_run {
            if super_branch.needs_fetch() {
                git_backend::fetch_remote_branch(&super_repo, "origin", branch)?;
            }
            let result = if !super_branch.local && create {
                git_backend::create_branch(&super_repo, branch, None)
            } else {
                git_backend::checkout_branch(&super_repo, branch)
            };
            if let Err(err) = result {
                Formatter::print_error(&format!(
                    "Failed to checkout '{}' in superproject: {}",
                    branch, err
                ));
                return Err(SuperGitError::Other(
                    "Superproject checkout failed".to_string(),
                ));
            }

            if super_branch.local || super_branch.remote_tracking || super_branch.remote_advertised
            {
                Formatter::print_success(&format!("Checked out '{}' in superproject", branch));
            } else {
                Formatter::print_success(&format!(
                    "Created and checked out '{}' in superproject",
                    branch
                ));
            }
            successful += 1;
        } else {
            if super_branch.local {
                Formatter::print_info(&format!("Would checkout '{}'", branch));
            } else if super_branch.remote_tracking {
                Formatter::print_info(&format!(
                    "Would checkout '{}' from remote-tracking branch",
                    branch
                ));
            } else if super_branch.remote_advertised {
                Formatter::print_info(&format!(
                    "Would fetch and checkout '{}' from remote",
                    branch
                ));
            } else {
                Formatter::print_info(&format!("Would create and checkout '{}'", branch));
            }
            successful += 1;
        }
    }

    // Parent before child: checking out a parent rewrites the gitlinks that tell
    // us which children exist at all, so children must be visited afterwards.
    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;
        }

        // Check for uncommitted changes
        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 branch_state = match branch_availability(&repo, branch, create) {
            Ok(state) => state,
            Err(e) => {
                Formatter::print_error(&format!(
                    "Failed to check branch in '{}': {}",
                    node.label(),
                    e
                ));
                failed += 1;
                continue;
            }
        };

        if !branch_state.available() && !create && ignore_missing {
            if verbose {
                Formatter::print_info(&format!(
                    "Branch '{}' does not exist in '{}', skipping",
                    branch,
                    node.label()
                ));
            }
            skipped += 1;
            continue;
        }

        if !branch_state.available() && !create {
            Formatter::print_error(&format!(
                "Branch '{}' does not exist in '{}'. Use --create to create it.",
                branch,
                node.label()
            ));
            failed += 1;
            continue;
        }

        let uncommitted = match GitOps::get_uncommitted_files(&repo) {
            Ok(files) => files,
            Err(e) => {
                Formatter::print_error(&format!(
                    "Failed to check status of '{}': {}",
                    node.label(),
                    e
                ));
                failed += 1;
                continue;
            }
        };

        if !uncommitted.is_empty() && !dry_run {
            // Name them: the reader is about to be asked whether to risk losing
            // these, and cannot answer that without knowing what they are.
            Formatter::print_warning(&format!(
                "Submodule '{}' has uncommitted changes: {}",
                node.label(),
                GitOps::describe_uncommitted(&uncommitted)
            ));

            let should_continue = Confirm::new()
                .with_prompt("Do you want to continue? (changes may be lost)")
                .default(false)
                .interact()
                .unwrap_or(false);

            if !should_continue {
                Formatter::print_info("Skipping");
                skipped += 1;
                continue;
            }
        }

        if dry_run {
            if branch_state.local {
                Formatter::print_info(&format!("Would checkout '{}'", branch));
            } else if branch_state.remote_tracking {
                Formatter::print_info(&format!(
                    "Would checkout '{}' from remote-tracking branch",
                    branch
                ));
            } else if branch_state.remote_advertised {
                Formatter::print_info(&format!(
                    "Would fetch and checkout '{}' from remote",
                    branch
                ));
            } else {
                Formatter::print_info(&format!("Would create and checkout '{}'", branch));
            }
            successful += 1;
            continue;
        }

        // Perform checkout
        if branch_state.needs_fetch() {
            if let Err(e) = git_backend::fetch_remote_branch(&repo, "origin", branch) {
                Formatter::print_error(&format!(
                    "Failed to fetch '{}' in '{}': {}",
                    branch,
                    node.label(),
                    e
                ));
                failed += 1;
                continue;
            }
        }

        match SubmoduleOps::checkout(node, branch, !branch_state.local && create) {
            Ok(_) => {
                if branch_state.available() {
                    Formatter::print_success(&format!(
                        "Checked out '{}' in '{}'",
                        branch,
                        node.label()
                    ));
                } else {
                    Formatter::print_success(&format!(
                        "Created and checked out '{}' in '{}'",
                        branch,
                        node.label()
                    ));
                }
                successful += 1;
            }
            Err(e) => {
                Formatter::print_error(&format!(
                    "Failed to checkout '{}' in '{}': {}",
                    branch,
                    node.label(),
                    e
                ));
                failed += 1;
            }
        }
    }

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

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

    Ok(())
}

fn branch_availability(
    repo: &gix::Repository,
    branch: &str,
    create: bool,
) -> Result<BranchAvailability> {
    let local = GitOps::branch_exists(repo, branch)?;
    if local || create {
        return Ok(BranchAvailability {
            local,
            remote_tracking: false,
            remote_advertised: false,
        });
    }

    let remote_tracking = GitOps::remote_branch_exists(repo, branch)?;
    if remote_tracking {
        return Ok(BranchAvailability {
            local,
            remote_tracking,
            remote_advertised: false,
        });
    }

    let remote_advertised = git_backend::remote_branch_head(repo, "origin", branch)?.is_some();
    Ok(BranchAvailability {
        local,
        remote_tracking,
        remote_advertised,
    })
}