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}