dev_prune/commands/
link.rs1use std::path::Path;
7
8use anyhow::{Context, Result};
9
10use crate::config::{PerRepoConfig, Registry};
11use crate::output;
12use crate::scanner;
13
14pub fn run_link(path_str: &str, quiet: bool) -> Result<()> {
20 let path = Path::new(path_str)
21 .canonicalize()
22 .with_context(|| format!("Path not found: {path_str}"))?;
23
24 if !scanner::is_git_repo(&path) {
25 if quiet {
26 return Ok(());
27 }
28 anyhow::bail!(
31 "`{}` is not a Git repository.\n \
32 Run `git init` there first, then `devp link .` again.",
33 output::clean_path(&path)
34 );
35 }
36
37 if quiet && is_under_temp_dir(&path) {
44 return Ok(());
45 }
46
47 if quiet
50 && !matches!(
51 PerRepoConfig::load_with_diagnostics(&path),
52 Ok(None)
53 | Ok(Some(PerRepoConfig {
54 disable_hooks: false,
55 ..
56 }))
57 )
58 {
59 return Ok(());
60 }
61
62 let mut registry = Registry::load()?;
63
64 if registry.add_repo(path.clone()) {
65 registry.last_added_repos = vec![path.clone()];
66 registry.save()?;
67 if !quiet {
68 output::print_success(&format!("Linked: {}", output::clean_path(&path)));
69 if registry.settings.auto_config {
70 ensure_default_repo_config(&path);
71 }
72 }
73 } else if !quiet {
74 output::print_info(&format!("Already linked: {}", output::clean_path(&path)));
75 }
76
77 Ok(())
78}
79
80pub(crate) fn ensure_default_repo_config(path: &Path) {
87 if path.join(crate::constants::PER_REPO_CONFIG_FILE).exists() {
88 return;
89 }
90 match PerRepoConfig::default().save_to_repo(path) {
91 Ok(()) => output::print_info(&format!(
92 "auto_config: wrote a default `.devprune.json` in {}",
93 output::clean_path(path)
94 )),
95 Err(e) => output::print_warning(&format!(
96 "auto_config: could not write `.devprune.json` in {}: {e}",
97 output::clean_path(path)
98 )),
99 }
100}
101
102fn is_under_temp_dir(path: &Path) -> bool {
109 std::env::temp_dir()
110 .canonicalize()
111 .is_ok_and(|tmp| path.starts_with(tmp))
112}
113
114pub fn run_unlink_missing() -> Result<()> {
121 let mut registry = Registry::load()?;
122
123 let gone: Vec<_> = registry
124 .repositories
125 .keys()
126 .filter(|p| !p.exists())
127 .cloned()
128 .collect();
129
130 if gone.is_empty() {
131 output::print_success("Every registered repository still exists — nothing to remove.");
132 return Ok(());
133 }
134
135 for path in &gone {
136 registry.remove_repo(path);
137 output::print_info(&format!("Unlinked: {}", output::clean_path(path)));
138 }
139 registry.last_added_repos.retain(|p| p.exists());
143 registry.save()?;
144
145 output::print_success(&format!(
146 "Removed {} registry {} pointing at directories that no longer exist.",
147 gone.len(),
148 output::plural(gone.len(), "entry", "entries")
149 ));
150 Ok(())
151}
152
153pub fn run_unlink(path_str: &str) -> Result<()> {
155 let path = Path::new(path_str)
158 .canonicalize()
159 .unwrap_or_else(|_| Path::new(path_str).to_path_buf());
160
161 let mut registry = Registry::load()?;
162
163 if registry.remove_repo(&path) {
164 registry.save()?;
165 output::print_success(&format!("Unlinked: {}", output::clean_path(&path)));
166 } else {
167 output::print_warning(&format!("Not in registry: {}", output::clean_path(&path)));
168 }
169
170 Ok(())
171}
172
173#[cfg(test)]
174mod tests {
175 use super::*;
176 use tempfile::TempDir;
177
178 #[test]
179 fn a_repository_under_the_temp_directory_is_recognised_as_scratch() {
180 let tmp = TempDir::new().unwrap();
181 let repo = tmp.path().canonicalize().unwrap().join("repo");
182 std::fs::create_dir_all(&repo).unwrap();
183
184 assert!(
185 is_under_temp_dir(&repo),
186 "{} should be seen as scratch — TempDir builds under std::env::temp_dir()",
187 repo.display()
188 );
189 }
190
191 #[test]
192 fn a_repository_outside_the_temp_directory_is_not() {
193 let here = Path::new(env!("CARGO_MANIFEST_DIR"))
195 .canonicalize()
196 .unwrap();
197 assert!(!is_under_temp_dir(&here));
198 }
199}