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,
init: bool,
recursive: bool,
dry_run: bool,
verbose: bool,
) -> Result<()> {
let mut failed = 0;
let mut skipped = 0;
if dry_run {
Formatter::print_warning("DRY RUN: No changes will be made");
return dry_run_report(&repo_root, init, recursive);
}
if init {
let outcome = SubmoduleOps::init_tree(&repo_root, recursive, |node| {
Formatter::print_info(&format!("Initializing '{}'...", node.label()));
})?;
for name in &outcome.initialized {
Formatter::print_success(&format!("Initialized '{}'", name));
}
for (name, err) in &outcome.failures {
Formatter::print_error(&format!("Failed to initialize '{}': {}", name, err));
}
failed += outcome.failures.len();
}
let tree = SubmoduleTree::discover(&repo_root)?;
Formatter::print_info(&format!(
"Synchronizing {} submodule(s) based on .gitmodules",
tree.len()
));
let mut successful = 0;
for node in tree.top_down() {
if verbose {
Formatter::print_submodule_header(node.label());
}
if !node.initialized {
Formatter::print_warning(&format!(
"Submodule '{}' not initialized. Use --init to initialize.",
node.label()
));
skipped += 1;
continue;
}
if verbose {
Formatter::print_info("Fetching...");
}
if let Err(e) = SubmoduleOps::fetch(node, false) {
Formatter::print_error(&format!("Failed to fetch '{}': {}", node.label(), e));
failed += 1;
continue;
}
let target_branch = match &node.branch {
Some(branch) => branch.clone(),
None => {
let repo = match GitOps::open_repo(&node.abs_path) {
Ok(repo) => repo,
Err(e) => {
Formatter::print_error(&format!(
"Failed to open '{}': {}",
node.label(),
e
));
failed += 1;
continue;
}
};
match GitOps::current_branch(&repo) {
Ok(branch) => branch,
Err(_) => {
Formatter::print_warning(&format!(
"No branch specified in .gitmodules and '{}' is not on a branch, skipping",
node.label()
));
skipped += 1;
continue;
}
}
}
};
if verbose {
Formatter::print_info(&format!("Checking out '{}'...", target_branch));
}
if let Err(e) = SubmoduleOps::checkout(node, &target_branch, false) {
Formatter::print_error(&format!(
"Failed to checkout '{}' in '{}': {}",
target_branch,
node.label(),
e
));
failed += 1;
continue;
}
if verbose {
Formatter::print_info("Pulling...");
}
match SubmoduleOps::pull(node, git_backend::PullOptions::default()) {
Ok(git_backend::PullOutcome::Diverged {
incoming,
outgoing,
}) => {
Formatter::print_error(&format!(
"'{}' has diverged on '{}': {incoming} incoming commit(s) and {outgoing} \
local commit(s) — rebase or merge them, then sync again",
node.label(),
target_branch
));
failed += 1;
}
Ok(_) => {
Formatter::print_success(&format!(
"Synced '{}' on branch '{}'",
node.label(),
target_branch
));
successful += 1;
}
Err(e) => {
Formatter::print_error(&format!("Failed to pull '{}': {}", node.label(), e));
failed += 1;
}
}
}
Formatter::print_summary(successful, failed, skipped);
if failed > 0 {
return Err(SuperGitError::Other(format!(
"{} submodule(s) failed to sync",
failed
)));
}
Ok(())
}
fn dry_run_report<P: AsRef<Path>>(repo_root: P, init: bool, recursive: bool) -> Result<()> {
let tree = SubmoduleTree::discover(&repo_root)?;
Formatter::print_info(&format!(
"Synchronizing {} submodule(s) based on .gitmodules",
tree.len()
));
let mut hidden_children = false;
for node in tree.top_down() {
if node.initialized {
Formatter::print_info(&format!("Would fetch and sync '{}'", node.label()));
continue;
}
if init {
Formatter::print_info(&format!("Would initialize '{}'", node.label()));
if recursive {
hidden_children = true;
}
} else {
Formatter::print_warning(&format!(
"Submodule '{}' not initialized. Use --init to initialize.",
node.label()
));
}
}
if hidden_children {
Formatter::print_info(
"Submodules nested inside the uninitialized ones above are not listed — they become \
visible only once their parent is checked out",
);
}
Ok(())
}