use std::fs;
use std::path::Path;
use std::process::ExitCode;
use anyhow::{Context, Result};
use serde::Serialize;
use crate::output::CommandReport;
use crate::paths::git as git_paths;
use crate::paths::state;
use crate::repo::marker as repo_marker;
use crate::repo::registry as repo_registry;
#[derive(Serialize)]
pub struct UnlinkReport {
command: &'static str,
ok: bool,
path: String,
marker_path: String,
locality_id: Option<String>,
unlinked: bool,
clone_state_root: Option<String>,
clone_state_present: bool,
warnings: Vec<String>,
}
impl CommandReport for UnlinkReport {
fn exit_code(&self) -> ExitCode {
ExitCode::SUCCESS
}
fn render_text(&self) {
if self.unlinked {
if let Some(locality_id) = &self.locality_id {
println!("Unlinked workspace from project ID {locality_id}.");
} else {
println!("Removed local CCD link marker.");
}
println!("Marker removed: {}", self.marker_path);
} else {
println!("No repo link found at {}", self.marker_path);
}
if let Some(root) = &self.clone_state_root {
println!("Workspace state root: {root}");
}
for warning in &self.warnings {
println!("Warning: {warning}");
}
}
}
pub fn run(repo_root: &Path) -> Result<UnlinkReport> {
let marker_path = repo_root.join(repo_marker::MARKER_FILE);
let marker = repo_marker::load(repo_root)?;
let mut warnings = Vec::new();
let (locality_id, unlinked) = match marker {
Some(marker) => {
if let Err(error) = remove_current_clone_root(repo_root, &marker.locality_id) {
warnings.push(format!(
"failed to prune current clone root from repo registry for locality_id `{}`: {error:#}",
marker.locality_id
));
}
fs::remove_file(&marker_path)
.with_context(|| format!("failed to remove {}", marker_path.display()))?;
(Some(marker.locality_id), true)
}
None => (None, false),
};
let clone_state_root = git_paths::ccd_dir(repo_root)
.ok()
.map(|path| path.join("profiles"));
let clone_state_present = clone_state_root
.as_ref()
.map(|path| path.exists())
.unwrap_or(false);
if unlinked && clone_state_present {
warnings.push(format!(
"workspace-local state remains at {}; run `ccd gc --path {}` to prune it if this workspace will stay detached",
clone_state_root
.as_ref()
.expect("clone state root should exist when present")
.display(),
repo_root.display()
));
}
Ok(UnlinkReport {
command: "unlink",
ok: true,
path: repo_root.display().to_string(),
marker_path: marker_path.display().to_string(),
locality_id,
unlinked,
clone_state_root: clone_state_root.map(|path| path.display().to_string()),
clone_state_present,
warnings,
})
}
fn remove_current_clone_root(repo_root: &Path, locality_id: &str) -> Result<()> {
let clone_root = repo_registry::normalize_clone_root(repo_root)?;
let registry_path = state::default_ccd_root()?
.join("repos")
.join(repo_marker::validate_locality_id(locality_id)?)
.join(repo_registry::REPO_METADATA_FILE);
let Some(mut entry) = repo_registry::load(®istry_path)? else {
return Ok(());
};
if entry.remove_known_clone_root(&clone_root) {
repo_registry::write(®istry_path, &entry)?;
}
Ok(())
}