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 }
70 } else if !quiet {
71 output::print_info(&format!("Already linked: {}", output::clean_path(&path)));
72 }
73
74 Ok(())
75}
76
77fn is_under_temp_dir(path: &Path) -> bool {
84 std::env::temp_dir()
85 .canonicalize()
86 .is_ok_and(|tmp| path.starts_with(tmp))
87}
88
89pub fn run_unlink_missing() -> Result<()> {
96 let mut registry = Registry::load()?;
97
98 let gone: Vec<_> = registry
99 .repositories
100 .keys()
101 .filter(|p| !p.exists())
102 .cloned()
103 .collect();
104
105 if gone.is_empty() {
106 output::print_success("Every registered repository still exists — nothing to remove.");
107 return Ok(());
108 }
109
110 for path in &gone {
111 registry.remove_repo(path);
112 output::print_info(&format!("Unlinked: {}", output::clean_path(path)));
113 }
114 registry.last_added_repos.retain(|p| p.exists());
118 registry.save()?;
119
120 output::print_success(&format!(
121 "Removed {} registry {} pointing at directories that no longer exist.",
122 gone.len(),
123 output::plural(gone.len(), "entry", "entries")
124 ));
125 Ok(())
126}
127
128pub fn run_unlink(path_str: &str) -> Result<()> {
130 let path = Path::new(path_str)
133 .canonicalize()
134 .unwrap_or_else(|_| Path::new(path_str).to_path_buf());
135
136 let mut registry = Registry::load()?;
137
138 if registry.remove_repo(&path) {
139 registry.save()?;
140 output::print_success(&format!("Unlinked: {}", output::clean_path(&path)));
141 } else {
142 output::print_warning(&format!("Not in registry: {}", output::clean_path(&path)));
143 }
144
145 Ok(())
146}
147
148#[cfg(test)]
149mod tests {
150 use super::*;
151 use tempfile::TempDir;
152
153 #[test]
154 fn a_repository_under_the_temp_directory_is_recognised_as_scratch() {
155 let tmp = TempDir::new().unwrap();
156 let repo = tmp.path().canonicalize().unwrap().join("repo");
157 std::fs::create_dir_all(&repo).unwrap();
158
159 assert!(
160 is_under_temp_dir(&repo),
161 "{} should be seen as scratch — TempDir builds under std::env::temp_dir()",
162 repo.display()
163 );
164 }
165
166 #[test]
167 fn a_repository_outside_the_temp_directory_is_not() {
168 let here = Path::new(env!("CARGO_MANIFEST_DIR"))
170 .canonicalize()
171 .unwrap();
172 assert!(!is_under_temp_dir(&here));
173 }
174}