Skip to main content

outpost_core/ops/
remove.rs

1use std::path::{Path, PathBuf};
2
3use crate::{safety, OutpostError, OutpostResult, SourceRepo};
4
5pub struct RemoveOptions {
6    pub path: PathBuf,
7    pub force: bool,
8}
9
10pub fn run(source: &SourceRepo, opts: RemoveOptions) -> OutpostResult<()> {
11    let mut registry = source.registry_mut()?;
12    let entry = registry_entry(registry.entries(), &opts.path)?.clone();
13
14    if entry.locked && !opts.force {
15        return Err(OutpostError::OutpostLocked {
16            path: entry.path,
17            reason: lock_reason(&entry.lock_reason),
18        });
19    }
20
21    if !entry.path.exists() {
22        registry.remove_by_path(&entry.path)?;
23        return registry.save();
24    }
25
26    let outpost = safety::check_path_is_managed_outpost_of(source, &entry.path)?;
27    if !opts.force {
28        safety::check_clean(outpost.work_tree(), outpost.git())?;
29        safety::check_no_unpushed(&outpost, source)?;
30    }
31
32    registry.remove_by_path(&entry.path)?;
33    registry.save()?;
34    std::fs::remove_dir_all(&entry.path).map_err(|source| OutpostError::IoAt {
35        path: entry.path,
36        source,
37    })
38}
39
40fn registry_entry<'a>(
41    entries: &'a [crate::RegistryEntry],
42    path: &Path,
43) -> OutpostResult<&'a crate::RegistryEntry> {
44    let lookup = canonicalize_existing_or_missing(path);
45    entries
46        .iter()
47        .find(|entry| entry.path == lookup)
48        .ok_or(OutpostError::RegistryEntryNotFound(lookup))
49}
50
51fn canonicalize_existing_or_missing(path: &Path) -> PathBuf {
52    match std::fs::canonicalize(path) {
53        Ok(canonical) => canonical,
54        Err(_) => match (path.parent(), path.file_name()) {
55            (Some(parent), Some(name)) => std::fs::canonicalize(parent)
56                .map(|parent| parent.join(Path::new(name)))
57                .unwrap_or_else(|_| path.to_path_buf()),
58            _ => path.to_path_buf(),
59        },
60    }
61}
62
63fn lock_reason(reason: &Option<String>) -> String {
64    reason
65        .as_ref()
66        .map(|reason| format!(": {reason}"))
67        .unwrap_or_default()
68}