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;
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;
}
}
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 {
Formatter::print_warning(&format!(
"Submodule '{}' has uncommitted changes, skipping",
node.label()
));
skipped += 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;
}
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;
}
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(())
}