git_worktree_manager/
registry.rs1use std::collections::HashMap;
4use std::path::{Path, PathBuf};
5
6use serde::{Deserialize, Serialize};
7
8use crate::constants::home_dir_or_fallback;
9use crate::error::Result;
10use crate::git;
11
12const REGISTRY_VERSION: u32 = 1;
13
14const SCAN_SKIP_DIRS: &[&str] = &[
16 "node_modules",
17 ".cache",
18 ".npm",
19 ".yarn",
20 "__pycache__",
21 ".venv",
22 "venv",
23 ".tox",
24 ".nox",
25 ".eggs",
26 "dist",
27 "build",
28 ".git",
29 "Library",
30 ".Trash",
31 ".local",
32 "Applications",
33 ".cargo",
34 ".rustup",
35 ".pyenv",
36 ".nvm",
37 ".rbenv",
38 ".goenv",
39 ".volta",
40 "site-packages",
41 ".mypy_cache",
42 ".ruff_cache",
43 ".pytest_cache",
44 "coverage",
45 ".next",
46 ".nuxt",
47 ".output",
48 ".turbo",
49];
50
51#[derive(Debug, Clone, Serialize, Deserialize)]
52pub struct RepoEntry {
53 pub name: String,
54 pub registered_at: String,
55 pub last_seen: String,
56}
57
58#[derive(Debug, Clone, Serialize, Deserialize)]
59pub struct Registry {
60 pub version: u32,
61 pub repositories: HashMap<String, RepoEntry>,
62}
63
64impl Default for Registry {
65 fn default() -> Self {
66 Self {
67 version: REGISTRY_VERSION,
68 repositories: HashMap::new(),
69 }
70 }
71}
72
73pub fn get_registry_path() -> PathBuf {
75 home_dir_or_fallback()
76 .join(".config")
77 .join("git-worktree-manager")
78 .join("registry.json")
79}
80
81pub fn load_registry() -> Registry {
83 let path = get_registry_path();
84 if !path.exists() {
85 return Registry::default();
86 }
87
88 match std::fs::read_to_string(&path) {
89 Ok(content) => serde_json::from_str(&content).unwrap_or_default(),
90 Err(_) => Registry::default(),
91 }
92}
93
94pub fn save_registry(registry: &Registry) -> Result<()> {
96 let path = get_registry_path();
97 if let Some(parent) = path.parent() {
98 std::fs::create_dir_all(parent)?;
99 }
100 let content = serde_json::to_string_pretty(registry)?;
101 std::fs::write(&path, content)?;
102 Ok(())
103}
104
105pub fn register_repo(repo_path: &Path) -> Result<()> {
107 let mut registry = load_registry();
108 let key = repo_path
109 .canonicalize()
110 .unwrap_or_else(|_| repo_path.to_path_buf())
111 .to_string_lossy()
112 .to_string();
113
114 let now = crate::session::chrono_now_iso_pub();
115
116 if let Some(entry) = registry.repositories.get_mut(&key) {
117 entry.last_seen = now;
118 } else {
119 let name = repo_path
120 .file_name()
121 .map(|n| n.to_string_lossy().to_string())
122 .unwrap_or_else(|| "unknown".to_string());
123
124 registry.repositories.insert(
125 key,
126 RepoEntry {
127 name,
128 registered_at: now.clone(),
129 last_seen: now,
130 },
131 );
132 }
133
134 save_registry(®istry)
135}
136
137pub fn update_last_seen(repo_path: &Path) -> Result<()> {
139 let mut registry = load_registry();
140 let key = repo_path
141 .canonicalize()
142 .unwrap_or_else(|_| repo_path.to_path_buf())
143 .to_string_lossy()
144 .to_string();
145
146 if let Some(entry) = registry.repositories.get_mut(&key) {
147 entry.last_seen = crate::session::chrono_now_iso_pub();
148 save_registry(®istry)?;
149 }
150 Ok(())
151}
152
153pub fn prune_registry() -> Result<Vec<String>> {
155 let mut registry = load_registry();
156 let mut removed = Vec::new();
157
158 let keys: Vec<String> = registry.repositories.keys().cloned().collect();
159 for key in keys {
160 let path = PathBuf::from(&key);
161 if !path.exists() || !path.join(".git").exists() {
162 registry.repositories.remove(&key);
163 removed.push(key);
164 }
165 }
166
167 if !removed.is_empty() {
168 save_registry(®istry)?;
169 }
170 Ok(removed)
171}
172
173pub fn get_all_registered_repos() -> Vec<(String, PathBuf)> {
175 let registry = load_registry();
176 registry
177 .repositories
178 .iter()
179 .map(|(path, entry)| (entry.name.clone(), PathBuf::from(path)))
180 .collect()
181}
182
183fn is_git_repo(path: &Path) -> bool {
185 path.join(".git").is_dir()
186}
187
188fn has_worktrees(repo_path: &Path) -> bool {
190 git::git_command(
191 &["worktree", "list", "--porcelain"],
192 Some(repo_path),
193 false,
194 true,
195 )
196 .map(|r| {
197 r.stdout
198 .lines()
199 .filter(|l| l.starts_with("worktree "))
200 .count()
201 > 1
202 })
203 .unwrap_or(false)
204}
205
206pub fn scan_for_repos(base_dir: Option<&Path>, max_depth: usize) -> Vec<PathBuf> {
208 let base = base_dir
209 .map(|p| p.to_path_buf())
210 .unwrap_or_else(home_dir_or_fallback);
211
212 let base = crate::git::canonicalize_or(&base);
213
214 let mut found = Vec::new();
215
216 fn scan_recursive(current: &Path, depth: usize, max_depth: usize, found: &mut Vec<PathBuf>) {
217 if depth > max_depth {
218 return;
219 }
220
221 let entries = match std::fs::read_dir(current) {
222 Ok(e) => e,
223 Err(_) => return,
224 };
225
226 let mut sorted: Vec<_> = entries.flatten().collect();
227 sorted.sort_by_key(|e| e.file_name());
228
229 for entry in sorted {
230 let path = entry.path();
231 if !path.is_dir() {
232 continue;
233 }
234
235 let name = entry.file_name();
236 let name_str = name.to_string_lossy();
237
238 if name_str.starts_with('.') || SCAN_SKIP_DIRS.contains(&name_str.as_ref()) {
239 continue;
240 }
241
242 if is_git_repo(&path) && has_worktrees(&path) {
243 found.push(path);
244 continue; }
246
247 scan_recursive(&path, depth + 1, max_depth, found);
248 }
249 }
250
251 scan_recursive(&base, 0, max_depth, &mut found);
252 found
253}