use std::path::Path;
use anyhow::{Context, Result};
use crate::config::{PerRepoConfig, Registry};
use crate::output;
use crate::scanner;
pub fn run_link(path_str: &str, quiet: bool) -> Result<()> {
let path = Path::new(path_str)
.canonicalize()
.with_context(|| format!("Path not found: {path_str}"))?;
if !scanner::is_git_repo(&path) {
if quiet {
return Ok(());
}
anyhow::bail!(
"`{}` is not a Git repository.\n \
Run `git init` there first, then `devp link .` again.",
output::clean_path(&path)
);
}
if quiet && is_under_temp_dir(&path) {
return Ok(());
}
if quiet
&& !matches!(
PerRepoConfig::load_with_diagnostics(&path),
Ok(None)
| Ok(Some(PerRepoConfig {
disable_hooks: false,
..
}))
)
{
return Ok(());
}
let mut registry = Registry::load()?;
if registry.add_repo(path.clone()) {
registry.last_added_repos = vec![path.clone()];
registry.save()?;
if !quiet {
output::print_success(&format!("Linked: {}", output::clean_path(&path)));
if registry.settings.auto_config {
ensure_default_repo_config(&path);
}
}
} else if !quiet {
output::print_info(&format!("Already linked: {}", output::clean_path(&path)));
}
Ok(())
}
pub(crate) fn ensure_default_repo_config(path: &Path) {
if path.join(crate::constants::PER_REPO_CONFIG_FILE).exists() {
return;
}
match PerRepoConfig::default().save_to_repo(path) {
Ok(()) => output::print_info(&format!(
"auto_config: wrote a default `.devprune.json` in {}",
output::clean_path(path)
)),
Err(e) => output::print_warning(&format!(
"auto_config: could not write `.devprune.json` in {}: {e}",
output::clean_path(path)
)),
}
}
fn is_under_temp_dir(path: &Path) -> bool {
std::env::temp_dir()
.canonicalize()
.is_ok_and(|tmp| path.starts_with(tmp))
}
pub fn run_unlink_missing() -> Result<()> {
let mut registry = Registry::load()?;
let gone: Vec<_> = registry
.repositories
.keys()
.filter(|p| !p.exists())
.cloned()
.collect();
if gone.is_empty() {
output::print_success("Every registered repository still exists — nothing to remove.");
return Ok(());
}
for path in &gone {
registry.remove_repo(path);
output::print_info(&format!("Unlinked: {}", output::clean_path(path)));
}
registry.last_added_repos.retain(|p| p.exists());
registry.save()?;
output::print_success(&format!(
"Removed {} registry {} pointing at directories that no longer exist.",
gone.len(),
output::plural(gone.len(), "entry", "entries")
));
Ok(())
}
pub fn run_unlink(path_str: &str) -> Result<()> {
let path = Path::new(path_str)
.canonicalize()
.unwrap_or_else(|_| Path::new(path_str).to_path_buf());
let mut registry = Registry::load()?;
if registry.remove_repo(&path) {
registry.save()?;
output::print_success(&format!("Unlinked: {}", output::clean_path(&path)));
} else {
output::print_warning(&format!("Not in registry: {}", output::clean_path(&path)));
}
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
use tempfile::TempDir;
#[test]
fn a_repository_under_the_temp_directory_is_recognised_as_scratch() {
let tmp = TempDir::new().unwrap();
let repo = tmp.path().canonicalize().unwrap().join("repo");
std::fs::create_dir_all(&repo).unwrap();
assert!(
is_under_temp_dir(&repo),
"{} should be seen as scratch — TempDir builds under std::env::temp_dir()",
repo.display()
);
}
#[test]
fn a_repository_outside_the_temp_directory_is_not() {
let here = Path::new(env!("CARGO_MANIFEST_DIR"))
.canonicalize()
.unwrap();
assert!(!is_under_temp_dir(&here));
}
}