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 MoveOptions {
    pub path: PathBuf,
    pub new_path: PathBuf,
    pub force: bool,
}

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

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

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

    std::fs::rename(&entry.path, &opts.new_path).map_err(|source| OutpostError::IoAt {
        path: entry.path.clone(),
        source,
    })?;
    registry.update_path(&entry.path, opts.new_path)?;
    registry.save()
}

fn registry_entry_index(entries: &[crate::RegistryEntry], path: &Path) -> OutpostResult<usize> {
    let canonical = std::fs::canonicalize(path)
        .map_err(|_| OutpostError::RegistryEntryNotFound(path.to_path_buf()))?;
    entries
        .iter()
        .position(|entry| entry.path == canonical)
        .ok_or(OutpostError::RegistryEntryNotFound(canonical))
}

fn check_destination_clean(destination: &Path) -> OutpostResult<()> {
    let parent = destination
        .parent()
        .filter(|path| !path.as_os_str().is_empty())
        .map(PathBuf::from)
        .unwrap_or_else(|| PathBuf::from("."));
    let name = destination.file_name().ok_or_else(|| OutpostError::IoAt {
        path: destination.to_path_buf(),
        source: std::io::Error::new(
            std::io::ErrorKind::InvalidInput,
            "destination path has no file name",
        ),
    })?;

    safety::check_destination_clean(&parent, Path::new(name)).map_err(|err| match err {
        OutpostError::DestinationExists(_) => {
            OutpostError::DestinationExists(destination.to_path_buf())
        }
        OutpostError::DestinationInsideRepo(_) => {
            OutpostError::DestinationInsideRepo(destination.to_path_buf())
        }
        other => other,
    })
}

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