use crate::config::Mount;
use crate::config::RepoConfigManager;
use crate::config::RepoMappingManager;
use crate::config::SyncStrategy;
use crate::git::GitSync;
use crate::git::utils::find_repo_root;
use crate::git::utils::get_control_repo_root;
use crate::git::utils::get_remote_url;
use crate::mount::MountResolver;
use crate::mount::MountSpace;
use anyhow::Context;
use anyhow::Result;
use colored::Colorize;
use std::env;
#[expect(
clippy::future_not_send,
reason = "git2::Repository is Send but not Sync; this is a known limitation"
)]
pub async fn execute(mount_name: Option<String>, all: bool) -> Result<()> {
if mount_name.is_some() && all {
anyhow::bail!("Cannot specify both a mount name and --all");
}
let code_root = find_repo_root(&env::current_dir()?)?;
let control_root = get_control_repo_root(&env::current_dir()?)?;
let repo_manager = RepoConfigManager::new(control_root.clone());
let resolver = MountResolver::new()?;
let desired = repo_manager
.load_desired_state()?
.ok_or_else(|| anyhow::anyhow!("No repository configuration found"))?;
let mut sync_list: Vec<(MountSpace, Mount)> = vec![];
if let Some(tm) = &desired.thoughts_mount
&& tm.sync == SyncStrategy::Auto
{
sync_list.push((
MountSpace::Thoughts,
Mount::Git {
url: tm.remote.clone(),
sync: tm.sync,
subpath: tm.subpath.clone(),
},
));
}
for cm in &desired.context_mounts {
if cm.sync == SyncStrategy::Auto {
sync_list.push((
MountSpace::Context(cm.mount_path.clone()),
Mount::Git {
url: cm.remote.clone(),
sync: cm.sync,
subpath: cm.subpath.clone(),
},
));
}
}
let mounts_to_sync: Vec<MountSpace> = if let Some(name) = mount_name {
let requested = MountSpace::parse(&name)?;
if sync_list.iter().any(|(space, _)| space == &requested) {
vec![requested]
} else {
anyhow::bail!("Mount '{name}' not found or not configured for sync");
}
} else if all {
sync_list.iter().map(|(space, _)| space.clone()).collect()
} else {
let repo_url = get_remote_url(&code_root)?;
println!("{} repository: {}", "Detected".cyan(), repo_url);
sync_list.iter().map(|(space, _)| space.clone()).collect()
};
if mounts_to_sync.is_empty() {
println!("{}: No mounts to sync", "Info".yellow());
if !all {
println!(
"Try '{}' to sync all auto-sync mounts",
"thoughts sync --all".cyan()
);
}
return Ok(());
}
println!("{} {} mount(s)...", "Syncing".green(), mounts_to_sync.len());
let mut any_failed = false;
for mount_space in &mounts_to_sync {
if let Some((_, mount)) = sync_list.iter().find(|(space, _)| space == mount_space) {
match sync_mount(&mount_space.as_str(), mount, &resolver).await {
Ok(()) => println!(" {} {}", "✓".green(), mount_space),
Err(e) => {
eprintln!(" {} {}: {}", "✗".red(), mount_space, e);
any_failed = true;
}
}
}
}
if any_failed {
anyhow::bail!("One or more mounts failed to sync");
}
Ok(())
}
#[expect(
clippy::future_not_send,
reason = "git2::Repository is Send but not Sync; this is a known limitation"
)]
async fn sync_mount(name: &str, mount: &Mount, resolver: &MountResolver) -> Result<()> {
let mount_path = resolver.resolve_mount(mount).context("Mount not cloned")?;
if !mount_path.exists() {
anyhow::bail!("Mount path does not exist: {}", mount_path.display());
}
match mount {
Mount::Git { url, subpath, .. } => {
let (repo_root, sync_subpath) = if let Some(sub) = subpath {
let repo_root = mount_path
.ancestors()
.find(|p| p.join(".git").exists())
.ok_or_else(|| anyhow::anyhow!("Could not find repository root"))?
.to_path_buf();
(repo_root, Some(sub.clone()))
} else {
(mount_path.clone(), None)
};
let git_sync = GitSync::new(&repo_root, sync_subpath)?;
git_sync.sync(name).await?;
let mut repo_mapping = RepoMappingManager::new()?;
repo_mapping.update_sync_time(url)?;
}
Mount::Directory { .. } => {
println!(" {} {} (directory mount)", "Skipping".dimmed(), name);
}
}
Ok(())
}