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}