1use crate::diagnostic_log::debug;
17use crate::fs_beneath::{device_of, handle_path, identity_of, open_directory_at, open_file_at, open_root, plain_components, remove_at};
18use crate::iso_time::now_iso;
19use crate::service_client::{Activation, ClientConfig, START_WAIT, manager_watch_for, wake};
20use crate::service_protocol::HEARTBEAT_SILENCE;
21use crate::service_messages::{ClientFailure, render};
22use crate::service_provision::{ProvisionHost, provision};
23use crate::service_route::{Route, route_from_environment};
24use crate::service_work::{Verdict, work_directory};
25use crate::transaction_git::GitContext;
26use crate::work_item::{PENDING, WorkItem, WorkKind, enqueue, state_directory};
27use crate::worktree_trash::TRASH_PARENT;
28use crate::worktree_trash_manifest::TrashManifest;
29use crate::parallel_budget::HostResources;
30use crate::worktree_trash_entries::{EntryRemoval, remove_entries};
31use crate::worktree_trash_remove::remove_tree_beneath;
32use std::fs::File;
33use std::io::Write;
34use std::path::{Path, PathBuf};
35
36const TAG: &str = "worktreeTrashDispose";
38
39pub const TRASH_REFERENCE: &str = "refs/cli-git/trash";
41
42const POINTER_LIMIT: u64 = 64 * 1024;
44
45pub type Router<'a> = &'a dyn Fn(&Path, &Path) -> Option<Route>;
47
48pub type Provisioner<'a> = &'a dyn Fn(&ProvisionHost) -> Result<(), ClientFailure>;
51
52pub struct Reach<'a> {
54 pub router: Router<'a>,
56 pub provisioner: Provisioner<'a>,
58 pub start_wait: std::time::Duration,
60}
61
62enum Handed {
64 Service,
66 Here {
69 record: Option<PathBuf>,
71 why: String,
73 },
74}
75
76pub fn dispose_pending_trash(context: &GitContext, destination: &Path, git_dir: &Path, trash: Option<&TrashManifest>, notices: &mut dyn Write) -> Result<(), String> {
88 let Some(manifest) = trash else { return Ok(()) };
89 return dispose_pending_trash_with(context.real_git.as_path(), destination, git_dir, manifest, notices, &Reach { router: &route_from_environment, provisioner: &provision, start_wait: START_WAIT });
90}
91
92pub fn dispose_pending_trash_with(real_git: &Path, destination: &Path, git_dir: &Path, manifest: &TrashManifest, notices: &mut dyn Write, reach: &Reach<'_>) -> Result<(), String> {
95 let (record, why): (Option<PathBuf>, String) = match hand_over(real_git, destination, git_dir, manifest, notices, reach) {
96 Handed::Service => return Ok(()),
97 Handed::Here { record, why } => (record, why),
98 };
99 debug(TAG, format!("the trash {} could not be handed to the service ({why}); emptying it here", manifest.name).as_str());
100 let _told = writeln!(notices, "cli-git: emptying the directories left out of the new worktree {} now, because the commit service could not take them over ({why}); this can take a while.", destination.display());
102 if let Err(reason) = delete_trash(destination, git_dir, manifest) {
103 let stays: String = match record {
104 Some(path) => format!("the work item {} stays, so the commit service empties it when it next runs for this worktree", path.display()),
105 None => String::from("no work item could be written for it, so it stays until it is removed by hand"),
106 };
107 return Err(format!("the directories left out of the new worktree {} could not be emptied ({reason}); {stays}", destination.display()));
108 }
109 if let Some(path) = record
111 && let Err(error) = std::fs::remove_file(&path)
112 && error.kind() != std::io::ErrorKind::NotFound
113 {
114 debug(TAG, format!("the work item {} could not be withdrawn ({error}); the service finds the trash gone", path.display()).as_str());
115 }
116 return Ok(());
117}
118
119fn hand_over(real_git: &Path, destination: &Path, git_dir: &Path, manifest: &TrashManifest, notices: &mut dyn Write, reach: &Reach<'_>) -> Handed {
125 let (Some(repository), Some(git_dir_text)) = (destination.to_str(), git_dir.to_str()) else {
127 debug(TAG, format!("{} or its administrative directory is not valid UTF-8, so no work item can name it", destination.display()).as_str());
128 return Handed::Here { record: None, why: String::from("the path of the worktree is not valid UTF-8, so a work item cannot name it") };
129 };
130 let item: WorkItem = WorkItem {
131 kind: WorkKind::Trash,
132 repository: repository.to_string(),
133 git_dir: git_dir_text.to_string(),
134 branch_ref: String::from(TRASH_REFERENCE),
135 oid: manifest.name.clone(),
136 parent: manifest.encode(),
137 created: now_iso(),
138 tries: 0,
139 };
140 if let Err(reason) = enqueue(work_directory(git_dir).as_path(), &item) {
141 debug(TAG, format!("the work item could not be written: {reason}").as_str());
142 return Handed::Here { record: None, why: format!("the work item could not be written: {reason}") };
143 }
144 let record: PathBuf = state_directory(work_directory(git_dir).as_path(), PENDING).join(item.id());
145 let Some(Route { address, provision: host }) = (reach.router)(real_git, git_dir) else {
146 return Handed::Here { record: Some(record), why: String::from("no commit service can be started from this session") };
147 };
148 if let Some(setup) = host.as_ref()
149 && let Err(failure) = (reach.provisioner)(setup)
150 {
151 return Handed::Here { record: Some(record), why: render(&failure) };
152 }
153 let config: ClientConfig = ClientConfig {
154 address,
155 activation: Activation::Managed,
156 start_wait: reach.start_wait,
157 heartbeat_silence: HEARTBEAT_SILENCE,
158 start_watch: host.as_ref().map(manager_watch_for),
159 keep_fresh: None, answer_ceiling: crate::wait_ceilings::ANSWER_CEILING, withdraw_ceiling: crate::wait_ceilings::WITHDRAW_CEILING, extra_wait: crate::wait_ceilings::SHORT_EXTRA_WAIT, start_ceiling: crate::wait_ceilings::START_CEILING,
160 };
161 return match wake(&config, git_dir, None, notices) {
162 Ok(()) => Handed::Service,
163 Err(failure) => Handed::Here { record: Some(record), why: render(&failure) },
164 };
165}
166
167fn is_not_empty(error: &std::io::Error) -> bool {
169 return matches!(error.raw_os_error(), Some(libc::ENOTEMPTY | libc::EEXIST));
170}
171
172fn verify_association(root: &File, repository: &Path, git_dir: &Path) -> Result<(), String> {
175 let pointer: File = open_file_at(root, std::ffi::OsStr::new(".git")).map_err(|error: std::io::Error| { return format!("{} has no readable .git file ({error}), so it is no linked worktree and its trash is left alone", repository.display()); })?;
176 if !pointer.metadata().is_ok_and(|found: std::fs::Metadata| { return found.is_file(); }) {
177 return Err(format!("{}/.git is not a file, so its trash is left alone", repository.display()));
178 }
179 let mut text: String = String::new();
180 std::io::Read::read_to_string(&mut std::io::Read::take(&pointer, POINTER_LIMIT), &mut text).map_err(|error: std::io::Error| { return format!("{}/.git cannot be read ({error})", repository.display()); })?;
181 let Some(recorded) = text.strip_prefix("gitdir:").map(|rest: &str| { return rest.trim(); }) else {
182 return Err(format!("{}/.git does not point at an administrative directory, so its trash is left alone", repository.display()));
183 };
184 let pointed: PathBuf = if Path::new(recorded).is_absolute() { PathBuf::from(recorded) } else { repository.join(recorded) };
185 let same: bool = pointed == git_dir || std::fs::canonicalize(&pointed).ok().zip(std::fs::canonicalize(git_dir).ok()).is_some_and(|(left, right): (PathBuf, PathBuf)| { return left == right; });
186 if !same {
187 return Err(format!("{}/.git points at {} and not at {}, so its trash is left alone", repository.display(), pointed.display(), git_dir.display()));
188 }
189 return Ok(());
190}
191
192pub fn delete_trash(repository: &Path, git_dir: &Path, manifest: &TrashManifest) -> Result<(), String> {
205 let root: File = open_root(repository).map_err(|error: std::io::Error| { return format!("{} cannot be opened as the worktree ({error})", repository.display()); })?;
206 verify_association(&root, repository, git_dir)?;
207 let device: u64 = device_of(&root).map_err(|error: std::io::Error| { return format!("{} cannot be examined ({error})", repository.display()); })?;
208 let names = plain_components(Path::new(TRASH_PARENT)).map_err(|error: std::io::Error| { return format!("the trash parent is not a plain path ({error})"); })?;
209 let mut shown: PathBuf = repository.to_path_buf();
210 let mut holders: Vec<File> = vec![root];
211 for name in &names {
212 shown.push(name);
213 let Some(held) = holders.last() else { return Err(format!("{} cannot be held", shown.display())) };
214 let next: File = match open_directory_at(held, name.as_os_str()) {
215 Ok(opened) => opened,
216 Err(error) if error.kind() == std::io::ErrorKind::NotFound => return Ok(()),
217 Err(error) => return Err(format!("{} is not a plain directory ({error}), so the trash below it is left alone", shown.display())),
218 };
219 if device_of(&next).ok() != Some(device) {
220 return Err(format!("{} is on another device, so the trash below it is left alone", shown.display()));
221 }
222 holders.push(next);
223 }
224 let Some(parent) = holders.last() else { return Ok(()) };
225 let trash: File = match open_directory_at(parent, std::ffi::OsStr::new(manifest.name.as_str())) {
226 Ok(opened) => opened,
227 Err(error) if error.kind() == std::io::ErrorKind::NotFound => return Ok(()),
228 Err(error) => return Err(format!("{} is not a plain directory ({error}), so it is left alone", shown.join(&manifest.name).display())),
229 };
230 let found = identity_of(&trash).map_err(|error: std::io::Error| { return format!("{} cannot be examined ({error})", shown.join(&manifest.name).display()); })?;
231 if found != manifest.trash {
232 return Err(format!("{} is not the directory the snapshot made ({} instead of {}), so it is left alone", shown.join(&manifest.name).display(), found.encode(), manifest.trash.encode()));
233 }
234 let inside: PathBuf = shown.join(&manifest.name);
235 let left: Vec<String> = remove_entries(&EntryRemoval { inside: inside.as_path(), trash: &trash, manifest, host: &HostResources::read(), remove: &remove_tree_beneath });
236 if !left.is_empty() {
237 return Err(left.join("; "));
238 }
239 let strangers: Vec<String> = std::fs::read_dir(handle_path(&trash))
240 .map_err(|error: std::io::Error| { return format!("{} cannot be listed ({error})", inside.display()); })?
241 .flatten()
242 .map(|entry: std::fs::DirEntry| { return entry.file_name().to_string_lossy().into_owned(); })
243 .collect();
244 if !strangers.is_empty() {
245 return Err(format!("{} also holds {}, which the snapshot did not put there, so the trash directory is left alone", inside.display(), strangers.join(", ")));
246 }
247 drop(trash);
248 remove_at(parent, std::ffi::OsStr::new(manifest.name.as_str()), true).map_err(|error: std::io::Error| { return format!("{} cannot be removed ({error})", inside.display()); })?;
249 if let [.., above, _] = holders.as_slice()
251 && let Some(last) = names.last()
252 && let Err(error) = remove_at(above, last.as_os_str(), true)
253 && !is_not_empty(&error)
254 {
255 debug(TAG, format!("{} cannot be removed ({error}); it stays", shown.display()).as_str());
256 }
257 return Ok(());
258}
259
260pub fn empty_trash(item: &WorkItem) -> Verdict {
272 let manifest: TrashManifest = match TrashManifest::decode(item.oid.as_str(), item.parent.as_str()) {
273 Ok(found) => found,
274 Err(reason) => return Verdict::Failed(reason),
275 };
276 return match delete_trash(Path::new(item.repository.as_str()), Path::new(item.git_dir.as_str()), &manifest) {
277 Ok(()) => Verdict::Done,
278 Err(reason) => Verdict::Failed(reason),
279 };
280}