wsb 0.222.4636

Workspace - A tool suite for file operations, version management, and development workflow automation
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::*;

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

    Formatter::print_info(&format!(
        "Pulling latest changes in superproject + {} submodule(s){}",
        tree.len(),
        if rebase { " (with rebase)" } 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;

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

    if !dry_run {
        let repo = GitOps::open_repo(&repo_root)?;
        let has_incoming = git_backend::has_incoming_commits(&repo, Some("origin"))?;
        if !force_all && !has_incoming {
            Formatter::print_info("No incoming commits in superproject, skipping pull");
            skipped += 1;
        } else {
            match git_backend::pull(&repo, rebase) {
                Ok(()) => {
                    Formatter::print_success("Pulled latest changes in superproject");
                    successful += 1;
                }
                Err(err) => {
                    let err_str = err.to_string();
                    if err_str.contains("Fast-forward merge is not possible") {
                        Formatter::print_error(
                            "Merge conflict in superproject. Resolve conflicts and try again.",
                        );
                        return Err(SuperGitError::MergeConflict("[SUPERPROJECT]".to_string()));
                    } else {
                        Formatter::print_error(&format!(
                            "Failed to pull in superproject: {}",
                            err_str
                        ));
                        failed += 1;
                    }
                }
            }
        }
    } else {
        let repo = GitOps::open_repo(&repo_root)?;
        if force_all || git_backend::has_incoming_commits(&repo, Some("origin"))? {
            Formatter::print_info("Would pull latest changes");
            successful += 1;
        } else {
            Formatter::print_info("No incoming commits in superproject, would skip pull");
            skipped += 1;
        }
    }

    // Parent before child: pulling a parent can move the gitlink a child is
    // expected to sit at, so the parent's new state must land first.
    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;
            }
        };

        // One status walk answers both "is it clean" and "what is dirty", and the
        // second question has to be answered anyway to say something actionable.
        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() {
            Formatter::print_warning(&format!(
                "Submodule '{}' has uncommitted changes, skipping: {}",
                node.label(),
                GitOps::describe_uncommitted(&uncommitted)
            ));
            skipped += 1;
            continue;
        }

        // Check if in detached HEAD
        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;
        }

        let has_incoming = match git_backend::has_incoming_commits(&repo, Some("origin")) {
            Ok(value) => value,
            Err(e) => {
                Formatter::print_error(&format!(
                    "Failed to determine incoming commits in '{}': {}",
                    node.label(),
                    e
                ));
                failed += 1;
                continue;
            }
        };

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

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

        // Perform pull
        match SubmoduleOps::pull(node, rebase) {
            Ok(_) => {
                Formatter::print_success(&format!("Pulled latest changes in '{}'", node.label()));
                successful += 1;
            }
            Err(SuperGitError::MergeConflict(_)) => {
                Formatter::print_error(&format!(
                    "Merge conflict in '{}'. Resolve conflicts and try again.",
                    node.label()
                ));
                failed += 1;
            }
            Err(e) => {
                Formatter::print_error(&format!("Failed to pull in '{}': {}", 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 pull",
            failed
        )));
    }

    Ok(())
}