Skip to main content

outpost_core/ops/
move.rs

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