outpost-core 0.1.3

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

use crate::selector::{OutpostSelector, resolve_entry};
use crate::{OutpostError, OutpostResult, SourceRepo, safety};

pub struct MoveOptions {
    pub selector: OutpostSelector,
    pub new_path: PathBuf,
    pub force: bool,
}

#[derive(Debug, Clone, PartialEq, Eq)]
pub struct MoveReport {
    pub old_path: PathBuf,
    pub new_path: PathBuf,
}

pub fn run(source: &SourceRepo, opts: MoveOptions) -> OutpostResult<MoveReport> {
    let entry = resolve_entry(source, &opts.selector)?.entry;
    if entry.locked && !opts.force {
        return Err(OutpostError::OutpostLocked {
            path: entry.path,
            reason: lock_reason(&entry.lock_reason),
        });
    }

    let outpost = safety::check_entry_is_managed_outpost_of(source, &entry)?;
    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,
    })?;
    let old_path = entry.path;
    let mut registry = source.registry_mut()?;
    registry.update_path(&old_path, opts.new_path.clone())?;
    registry.save()?;
    Ok(MoveReport {
        old_path,
        new_path: std::fs::canonicalize(&opts.new_path).map_err(|source| OutpostError::IoAt {
            path: opts.new_path,
            source,
        })?,
    })
}

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()
}