Skip to main content

thoughts_tool/mount/
auto_mount.rs

1use crate::config::Mount;
2use crate::config::RepoConfigManager;
3use crate::config::RepoMappingManager;
4use crate::config::SyncStrategy;
5use crate::config::extract_org_repo_from_url;
6use crate::git::clone::CloneOptions;
7use crate::git::clone::clone_repository;
8use crate::git::ref_key::encode_ref_key;
9use crate::git::utils::get_control_repo_root;
10use crate::mount::MountOptions;
11use crate::mount::MountResolver;
12use crate::mount::MountSpace;
13use crate::mount::get_mount_manager;
14use crate::platform::detect_platform;
15use crate::utils::paths::ensure_dir;
16use anyhow::Result;
17use colored::Colorize;
18use std::collections::HashMap;
19use std::path::PathBuf;
20
21pub async fn update_active_mounts() -> Result<()> {
22    let repo_root = get_control_repo_root(&std::env::current_dir()?)?;
23    let platform_info = detect_platform()?;
24    let mount_manager = get_mount_manager(&platform_info)?;
25    let repo_manager = RepoConfigManager::new(repo_root.clone());
26    let desired = repo_manager.load_desired_state()?.ok_or_else(|| {
27        anyhow::anyhow!("No repository configuration found. Run 'thoughts init'.")
28    })?;
29
30    let base = repo_root.join(".thoughts-data");
31
32    // Check for broken symlink before proceeding
33    if base.is_symlink() && !base.exists() {
34        anyhow::bail!(
35            "Worktree .thoughts-data symlink is broken. \
36             Re-run 'thoughts init' in the worktree or main repository."
37        );
38    }
39
40    ensure_dir(&base)?;
41
42    // Canonicalize base for mount comparison
43    let base_canon = std::fs::canonicalize(&base).unwrap_or_else(|_| base.clone());
44
45    // Symlink targets (actual mount dirs)
46    let thoughts_dir = base.join(&desired.mount_dirs.thoughts);
47    let context_dir = base.join(&desired.mount_dirs.context);
48    let references_dir = base.join(&desired.mount_dirs.references);
49    ensure_dir(&thoughts_dir)?;
50    ensure_dir(&context_dir)?;
51    ensure_dir(&references_dir)?;
52
53    println!("{} filesystem mounts...", "Synchronizing".cyan());
54
55    // Build desired targets with MountSpace
56    let mut desired_targets: Vec<(MountSpace, Mount, bool, Option<String>)> = vec![];
57
58    if let Some(tm) = &desired.thoughts_mount {
59        let m = Mount::Git {
60            url: tm.remote.clone(),
61            subpath: tm.subpath.clone(),
62            sync: tm.sync,
63        };
64        desired_targets.push((MountSpace::Thoughts, m, false, None));
65    }
66
67    for cm in &desired.context_mounts {
68        let m = Mount::Git {
69            url: cm.remote.clone(),
70            subpath: cm.subpath.clone(),
71            sync: cm.sync,
72        };
73        let space = MountSpace::Context(cm.mount_path.clone());
74        desired_targets.push((space, m, false, None));
75    }
76
77    for rm in &desired.references {
78        let url = &rm.remote;
79        let (org, repo) = match extract_org_repo_from_url(url) {
80            Ok(x) => x,
81            Err(e) => {
82                println!(
83                    "  {} Invalid reference in config, skipping: {}\n     {}",
84                    "Warning:".yellow(),
85                    url,
86                    e
87                );
88                continue;
89            }
90        };
91        let m = Mount::Git {
92            url: url.clone(),
93            subpath: None,
94            sync: SyncStrategy::None,
95        };
96        let ref_key = rm.ref_name.as_deref().map(encode_ref_key).transpose()?;
97        let space = MountSpace::Reference {
98            org_path: org,
99            repo,
100            ref_key,
101        };
102        desired_targets.push((space, m, true, rm.ref_name.clone()));
103    }
104
105    // Query active mounts and key them by relative path under .thoughts-data
106    let active = mount_manager.list_mounts().await?;
107    let mut active_map = HashMap::<String, PathBuf>::new();
108    for mi in active {
109        // Canonicalize target for comparison
110        let target_canon = std::fs::canonicalize(&mi.target).unwrap_or_else(|_| mi.target.clone());
111        if target_canon.starts_with(&base_canon)
112            && let Ok(rel) = target_canon.strip_prefix(&base_canon)
113        {
114            let key = rel.to_string_lossy().to_string();
115            active_map.insert(key, mi.target.clone());
116        }
117    }
118
119    // Unmount no-longer-desired
120    for (active_key, target_path) in &active_map {
121        if !desired_targets
122            .iter()
123            .any(|(space, _, _, _)| space.relative_path(&desired.mount_dirs) == *active_key)
124        {
125            println!("  {} removed mount: {}", "Unmounting".yellow(), active_key);
126            mount_manager.unmount(target_path, false).await?;
127        }
128    }
129
130    // Mount missing targets
131    let resolver = MountResolver::new()?;
132    for (space, m, _read_only, ref_name) in &desired_targets {
133        let key = space.relative_path(&desired.mount_dirs);
134        if !active_map.contains_key(&key) {
135            let target = desired.get_mount_target(space, &repo_root);
136            let parent = target.parent().ok_or_else(|| {
137                anyhow::anyhow!("mount target has no parent directory: {}", target.display())
138            })?;
139            ensure_dir(parent)?;
140
141            // Resolve mount source
142            let src = match (space, m) {
143                (MountSpace::Reference { .. }, Mount::Git { url, .. }) => {
144                    let repo_mapping = RepoMappingManager::new()?;
145                    if let Some(path) =
146                        repo_mapping.resolve_reference_url(url, ref_name.as_deref())?
147                    {
148                        path
149                    } else {
150                        println!("  {} repository {} ...", "Cloning".yellow(), url);
151                        clone_and_map(url, ref_name.as_deref())?
152                    }
153                }
154                _ => match resolver.resolve_mount(m) {
155                    Ok(p) => p,
156                    Err(_) => {
157                        if let Mount::Git { url, .. } = &m {
158                            println!("  {} repository {} ...", "Cloning".yellow(), url);
159                            clone_and_map(url, None)?
160                        } else {
161                            continue;
162                        }
163                    }
164                },
165            };
166
167            // Mount with appropriate options
168            let mut options = MountOptions::default();
169            if space.is_read_only() {
170                options.read_only = true;
171            }
172
173            println!(
174                "  {} {}: {}",
175                "Mounting".green(),
176                space,
177                if space.is_read_only() {
178                    "(read-only)"
179                } else {
180                    ""
181                }
182            );
183
184            match mount_manager.mount(&[src], &target, &options).await {
185                Ok(()) => println!("    {} Successfully mounted", "✓".green()),
186                Err(e) => eprintln!("    {} Failed to mount: {}", "✗".red(), e),
187            }
188        }
189    }
190
191    println!("{} Mount synchronization complete", "✓".green());
192    Ok(())
193}
194
195fn clone_and_map(url: &str, ref_name: Option<&str>) -> Result<PathBuf> {
196    let mut repo_mapping = RepoMappingManager::new()?;
197    let default_path = RepoMappingManager::get_default_reference_clone_path(url, ref_name)?;
198
199    // Clone to default location
200    let clone_opts = CloneOptions {
201        url: url.to_string(),
202        target_path: default_path.clone(),
203        branch: ref_name.map(str::to_string),
204    };
205    clone_repository(&clone_opts)?;
206
207    // Add mapping
208    repo_mapping.add_reference_mapping(url, ref_name, default_path.clone(), true)?;
209
210    Ok(default_path)
211}