Skip to main content

outpost_core/ops/
move.rs

1use std::path::{Path, PathBuf};
2
3use crate::{safety, OutpostError, OutpostResult, SourceRepo};
4
5pub struct MoveOptions {
6    pub path: PathBuf,
7    pub new_path: PathBuf,
8    pub force: bool,
9}
10
11pub fn run(source: &SourceRepo, opts: MoveOptions) -> OutpostResult<()> {
12    let mut registry = source.registry_mut()?;
13    let index = registry_entry_index(registry.entries(), &opts.path)?;
14    let entry = registry.entries()[index].clone();
15
16    if entry.locked && !opts.force {
17        return Err(OutpostError::OutpostLocked {
18            path: entry.path,
19            reason: lock_reason(&entry.lock_reason),
20        });
21    }
22
23    let outpost = safety::check_path_is_managed_outpost_of(source, &entry.path)?;
24    if !opts.force {
25        safety::check_clean(outpost.work_tree(), outpost.git())?;
26    }
27    check_destination_clean(&opts.new_path)?;
28
29    std::fs::rename(&entry.path, &opts.new_path).map_err(|source| OutpostError::IoAt {
30        path: entry.path.clone(),
31        source,
32    })?;
33    registry.update_path(&entry.path, opts.new_path)?;
34    registry.save()
35}
36
37fn registry_entry_index(entries: &[crate::RegistryEntry], path: &Path) -> OutpostResult<usize> {
38    let canonical = std::fs::canonicalize(path)
39        .map_err(|_| OutpostError::RegistryEntryNotFound(path.to_path_buf()))?;
40    entries
41        .iter()
42        .position(|entry| entry.path == canonical)
43        .ok_or(OutpostError::RegistryEntryNotFound(canonical))
44}
45
46fn check_destination_clean(destination: &Path) -> OutpostResult<()> {
47    let parent = destination
48        .parent()
49        .filter(|path| !path.as_os_str().is_empty())
50        .map(PathBuf::from)
51        .unwrap_or_else(|| PathBuf::from("."));
52    let name = destination.file_name().ok_or_else(|| OutpostError::IoAt {
53        path: destination.to_path_buf(),
54        source: std::io::Error::new(
55            std::io::ErrorKind::InvalidInput,
56            "destination path has no file name",
57        ),
58    })?;
59
60    safety::check_destination_clean(&parent, Path::new(name)).map_err(|err| match err {
61        OutpostError::DestinationExists(_) => {
62            OutpostError::DestinationExists(destination.to_path_buf())
63        }
64        OutpostError::DestinationInsideRepo(_) => {
65            OutpostError::DestinationInsideRepo(destination.to_path_buf())
66        }
67        other => other,
68    })
69}
70
71fn lock_reason(reason: &Option<String>) -> String {
72    reason
73        .as_ref()
74        .map(|reason| format!(": {reason}"))
75        .unwrap_or_default()
76}