Skip to main content

thoughts_tool/mount/
auto_mount.rs

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