use crate::config::RepoConfigManager;
use crate::config::RepoMappingManager;
use crate::config::validation::validate_reference_url;
use crate::git::clone::CloneOptions;
use crate::git::clone::clone_repository;
use crate::git::pull::pull_ff_only;
use crate::git::utils::HeadState;
use crate::git::utils::get_control_repo_root;
use crate::git::utils::get_head_state;
use anyhow::Result;
use colored::Colorize;
use thoughts_tool::repo_identity::RepoIdentity;
#[expect(clippy::unused_async, reason = "async for command API consistency")]
pub async fn execute(verbose: bool) -> Result<()> {
let repo_root = get_control_repo_root(&std::env::current_dir()?)?;
let mgr = RepoConfigManager::new(repo_root);
let mut mapping_mgr = RepoMappingManager::new()?;
let ds = mgr.load_desired_state()?.ok_or_else(|| {
anyhow::anyhow!("No repository configuration found. Run 'thoughts init'.")
})?;
if ds.references.is_empty() {
println!("No references configured.");
return Ok(());
}
let mut cloned_count = 0;
let mut updated_count = 0;
let mut skipped_count = 0;
let mut invalid_count = 0;
for rm in &ds.references {
let url = &rm.remote;
let ref_name = rm.ref_name.as_deref();
if let Err(e) = validate_reference_url(url) {
println!(
"{} Skipping invalid reference: {}\n{}",
"⚠".yellow(),
url,
e
);
invalid_count += 1;
continue;
}
if verbose && let Ok(id) = RepoIdentity::parse(url) {
let key = id.canonical_key();
println!("Reference: {url}");
println!(" canonical: {}/{}/{}", key.host, key.org_path, key.repo);
if let Some(ref_name) = ref_name {
println!(" ref: {ref_name}");
}
}
if let Some(local_path) = mapping_mgr.resolve_reference_url(url, ref_name)? {
if verbose {
println!(" mapping: {}", local_path.display());
}
match get_head_state(&local_path) {
Ok(HeadState::Attached(branch)) => {
match pull_ff_only(&local_path, "origin", Some(&branch)) {
Ok(()) => {
if verbose {
println!(" action: updated (ff-only)");
} else {
println!("{} Updated {} (ff-only)", "↻".green(), url);
}
updated_count += 1;
if let Err(e) = mapping_mgr.update_reference_sync_time(url, ref_name) {
tracing::warn!(
url = %url,
ref_name = ?ref_name,
error = ?e,
"Failed to update repos.json last_sync (continuing)"
);
}
}
Err(e) => {
if verbose {
println!(" action: FAILED - {e}");
} else {
println!(
"{} Failed to update {} (continuing): {}",
"✗".red(),
url,
e
);
}
skipped_count += 1;
}
}
}
Ok(HeadState::Detached | HeadState::Unborn(_)) | Err(_) => {
if verbose {
println!(" action: skipped (non-standard HEAD state)");
} else {
println!(
"{} Skipping update (non-standard HEAD state): {}",
"⚠".yellow(),
url
);
}
skipped_count += 1;
}
}
continue;
}
let default_path = RepoMappingManager::get_default_reference_clone_path(url, ref_name)?;
if verbose {
println!(" mapping: (none)");
println!(" action: cloning to {}", default_path.display());
}
match clone_repository(&CloneOptions {
url: url.clone(),
target_path: default_path.clone(),
branch: ref_name.map(std::string::ToString::to_string),
}) {
Ok(()) => {
mapping_mgr.add_reference_mapping(url, ref_name, default_path, true)?;
if !verbose {
println!("{} Cloned {}", "✓".green(), url);
}
cloned_count += 1;
if let Err(e) = mapping_mgr.update_reference_sync_time(url, ref_name) {
tracing::warn!(
url = %url,
ref_name = ?ref_name,
error = ?e,
"Failed to update repos.json last_sync (continuing)"
);
}
}
Err(e) => {
if verbose {
println!(" action: FAILED - {e}");
} else {
println!("{} Failed to clone {}: {}", "✗".red(), url, e);
}
skipped_count += 1;
}
}
}
println!();
println!(
"Cloned: {cloned_count}, Updated: {updated_count}, Skipped: {skipped_count}, Invalid: {invalid_count}"
);
if cloned_count + updated_count > 0 {
println!("Run 'thoughts mount update' to mount the references.");
}
Ok(())
}