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;
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;
}
}
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 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 {
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;
}
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,
})
}