git_policy_cli/worktree/
worktree_trash_remove.rs1use crate::fs_beneath::{ObjectId, device_of, handle_path, identity_at, identity_of, open_directory_at, remove_at};
17use std::ffi::{OsStr, OsString};
18use std::fs::File;
19use std::os::unix::fs::PermissionsExt;
20use std::path::{Path, PathBuf};
21
22const TAG: &str = "worktreeTrashRemove";
24
25#[derive(Clone, Copy, Debug, Eq, PartialEq)]
27pub enum Removal {
28 Removed,
30 Absent,
32}
33
34type Describe<'a> = &'a dyn Fn() -> PathBuf;
36
37struct Frame {
39 dir: File,
41 name: OsString,
43 entries: Vec<(OsString, bool)>,
45}
46
47fn is_gone(error: &std::io::Error) -> bool {
49 return error.kind() == std::io::ErrorKind::NotFound;
50}
51
52fn list_entries(dir: &File, shown: Describe<'_>) -> Result<Vec<(OsString, bool)>, String> {
54 let listing = std::fs::read_dir(handle_path(dir)).map_err(|error: std::io::Error| { return format!("{} cannot be listed ({error})", shown().display()); })?;
55 let mut entries: Vec<(OsString, bool)> = Vec::new();
56 for listed in listing {
57 let entry: std::fs::DirEntry = listed.map_err(|error: std::io::Error| { return format!("{} cannot be listed ({error})", shown().display()); })?;
58 let is_directory: bool = entry.file_type().map_err(|error: std::io::Error| { return format!("{} cannot be examined ({error})", shown().join(entry.file_name()).display()); })?.is_dir();
59 entries.push((entry.file_name(), is_directory));
60 }
61 return Ok(entries);
62}
63
64fn make_writable(dir: &File, shown: Describe<'_>) {
67 let Ok(metadata) = dir.metadata() else { return };
68 let mode: u32 = metadata.permissions().mode();
69 if mode & 0o700 == 0o700 {
70 return;
71 }
72 if let Err(error) = dir.set_permissions(std::fs::Permissions::from_mode(mode | 0o700)) {
73 crate::diagnostic_log::debug(TAG, format!("{} keeps its mode ({error})", shown().display()).as_str());
74 }
75}
76
77fn enter(parent: &File, name: &OsStr, device: u64, shown: Describe<'_>) -> Result<Frame, String> {
79 let dir: File = open_directory_at(parent, name).map_err(|error: std::io::Error| { return format!("{} is not a plain directory that can be entered, so it is left alone ({error})", shown().display()); })?;
80 let found: u64 = device_of(&dir).map_err(|error: std::io::Error| { return format!("{} cannot be examined ({error})", shown().display()); })?;
81 if found != device {
82 return Err(format!("{} is on another device, so it is left alone", shown().display()));
83 }
84 make_writable(&dir, shown);
85 let entries: Vec<(OsString, bool)> = list_entries(&dir, shown)?;
86 return Ok(Frame { dir, name: name.to_os_string(), entries });
87}
88
89pub fn remove_tree_beneath(label: &Path, parent: &File, name: &OsStr, expected: Option<ObjectId>) -> Result<Removal, String> {
102 let top: PathBuf = label.join(name);
103 let device: u64 = device_of(parent).map_err(|error: std::io::Error| { return format!("{} cannot be examined ({error})", label.display()); })?;
104 match identity_at(parent, name) {
105 Ok(found) => {
106 if let Some(wanted) = expected && found != wanted {
107 return Err(format!("{} is not the object that was set aside ({} instead of {}), so it is left alone", top.display(), found.encode(), wanted.encode()));
108 }
109 }
110 Err(error) if is_gone(&error) => return Ok(Removal::Absent),
111 Err(error) => return Err(format!("{} cannot be examined ({error})", top.display())),
112 }
113 let first: Frame = enter(parent, name, device, &|| { return top.clone(); })?;
114 if let Some(wanted) = expected {
115 let opened: ObjectId = identity_of(&first.dir).map_err(|error: std::io::Error| { return format!("{} cannot be examined ({error})", top.display()); })?;
116 if opened != wanted {
117 return Err(format!("{} is not the object that was set aside ({} instead of {}), so it is left alone", top.display(), opened.encode(), wanted.encode()));
118 }
119 }
120 let mut stack: Vec<Frame> = vec![first];
121 let mut shown: Vec<OsString> = Vec::new();
123 let here = |names: &[OsString]| -> PathBuf { return names.iter().fold(top.clone(), |path: PathBuf, part: &OsString| { return path.join(part); }); };
125 loop {
126 let Some(frame) = stack.last_mut() else { break };
127 match frame.entries.pop() {
128 Some((entry, true)) => {
129 let next: Frame = enter(&frame.dir, entry.as_os_str(), device, &|| { return here(shown.as_slice()).join(&entry); })?;
130 shown.push(entry);
131 stack.push(next);
132 }
133 Some((entry, false)) => match remove_at(&frame.dir, entry.as_os_str(), false) {
134 Ok(()) => {}
135 Err(error) if is_gone(&error) => {}
136 Err(error) => return Err(format!("{} cannot be removed ({error})", here(shown.as_slice()).join(&entry).display())),
137 },
138 None => {
139 let Some(done) = stack.pop() else { break };
140 let holder: &File = stack.last().map_or(parent, |above: &Frame| { return &above.dir; });
141 match remove_at(holder, done.name.as_os_str(), true) {
142 Ok(()) => {}
143 Err(error) if is_gone(&error) => {}
144 Err(error) => return Err(format!("{} cannot be removed ({error})", here(shown.as_slice()).display())),
145 }
146 shown.pop();
147 }
148 }
149 }
150 return Ok(Removal::Removed);
151}
152
153#[cfg(test)]
155#[path = "worktree_trash_remove_tests.rs"]
156mod tests;