outpost-core 0.1.0

Core library for Git Outpost, a clone-backed alternative to git worktree workflows.
Documentation
use std::path::{Path, PathBuf};

use crate::{safety, OutpostError, OutpostResult, SourceRepo};

pub struct RemoveOptions {
    pub path: PathBuf,
    pub force: bool,
}

pub fn run(source: &SourceRepo, opts: RemoveOptions) -> OutpostResult<()> {
    let mut registry = source.registry_mut()?;
    let entry = registry_entry(registry.entries(), &opts.path)?.clone();

    if entry.locked && !opts.force {
        return Err(OutpostError::OutpostLocked {
            path: entry.path,
            reason: lock_reason(&entry.lock_reason),
        });
    }

    if !entry.path.exists() {
        registry.remove_by_path(&entry.path)?;
        return registry.save();
    }

    let outpost = safety::check_path_is_managed_outpost_of(source, &entry.path)?;
    if !opts.force {
        safety::check_clean(outpost.work_tree(), outpost.git())?;
        safety::check_no_unpushed(&outpost, source)?;
    }

    registry.remove_by_path(&entry.path)?;
    registry.save()?;
    std::fs::remove_dir_all(&entry.path).map_err(|source| OutpostError::IoAt {
        path: entry.path,
        source,
    })
}

fn registry_entry<'a>(
    entries: &'a [crate::RegistryEntry],
    path: &Path,
) -> OutpostResult<&'a crate::RegistryEntry> {
    let lookup = canonicalize_existing_or_missing(path);
    entries
        .iter()
        .find(|entry| entry.path == lookup)
        .ok_or(OutpostError::RegistryEntryNotFound(lookup))
}

fn canonicalize_existing_or_missing(path: &Path) -> PathBuf {
    match std::fs::canonicalize(path) {
        Ok(canonical) => canonical,
        Err(_) => match (path.parent(), path.file_name()) {
            (Some(parent), Some(name)) => std::fs::canonicalize(parent)
                .map(|parent| parent.join(Path::new(name)))
                .unwrap_or_else(|_| path.to_path_buf()),
            _ => path.to_path_buf(),
        },
    }
}

fn lock_reason(reason: &Option<String>) -> String {
    reason
        .as_ref()
        .map(|reason| format!(": {reason}"))
        .unwrap_or_default()
}