use super::diagnostic_log::debug;
use super::fs_beneath::{device_of, handle_path, identity_of, open_directory_at, open_file_at, open_root, plain_components, remove_at};
use super::iso_time::now_iso;
use super::service_client::{Activation, COMMIT_WAIT, ClientConfig, START_WAIT, wake};
use super::service_messages::{ClientFailure, render};
use super::service_provision::{ProvisionHost, provision};
use super::service_route::{Route, route_from_environment};
use super::service_work::{Verdict, work_directory};
use super::transaction_git::GitContext;
use super::work_item::{PENDING, WorkItem, WorkKind, enqueue, state_directory};
use super::worktree_trash::TRASH_PARENT;
use super::worktree_trash_manifest::TrashManifest;
use super::parallel_budget::HostResources;
use super::worktree_trash_entries::{EntryRemoval, remove_entries};
use super::worktree_trash_remove::remove_tree_beneath;
use std::fs::File;
use std::io::Write;
use std::path::{Path, PathBuf};
const TAG: &str = "worktreeTrashDispose";
pub const TRASH_REFERENCE: &str = "refs/cli-git/trash";
const POINTER_LIMIT: u64 = 64 * 1024;
pub type Router<'a> = &'a dyn Fn(&Path, &Path) -> Option<Route>;
pub type Provisioner<'a> = &'a dyn Fn(&ProvisionHost) -> Result<(), ClientFailure>;
pub struct Reach<'a> {
pub router: Router<'a>,
pub provisioner: Provisioner<'a>,
pub start_wait: std::time::Duration,
}
enum Handed {
Service,
Here {
record: Option<PathBuf>,
why: String,
},
}
pub fn dispose_pending_trash(context: &GitContext, destination: &Path, git_dir: &Path, trash: Option<&TrashManifest>, notices: &mut dyn Write) -> Result<(), String> {
let Some(manifest) = trash else { return Ok(()) };
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 });
}
pub fn dispose_pending_trash_with(real_git: &Path, destination: &Path, git_dir: &Path, manifest: &TrashManifest, notices: &mut dyn Write, reach: &Reach<'_>) -> Result<(), String> {
let (record, why): (Option<PathBuf>, String) = match hand_over(real_git, destination, git_dir, manifest, notices, reach) {
Handed::Service => return Ok(()),
Handed::Here { record, why } => (record, why),
};
debug(TAG, format!("the trash {} could not be handed to the service ({why}); emptying it here", manifest.name).as_str());
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());
if let Err(reason) = delete_trash(destination, git_dir, manifest) {
let stays: String = match record {
Some(path) => format!("the work item {} stays, so the commit service empties it when it next runs for this worktree", path.display()),
None => String::from("no work item could be written for it, so it stays until it is removed by hand"),
};
return Err(format!("the directories left out of the new worktree {} could not be emptied ({reason}); {stays}", destination.display()));
}
if let Some(path) = record
&& let Err(error) = std::fs::remove_file(&path)
&& error.kind() != std::io::ErrorKind::NotFound
{
debug(TAG, format!("the work item {} could not be withdrawn ({error}); the service finds the trash gone", path.display()).as_str());
}
return Ok(());
}
fn hand_over(real_git: &Path, destination: &Path, git_dir: &Path, manifest: &TrashManifest, notices: &mut dyn Write, reach: &Reach<'_>) -> Handed {
let (Some(repository), Some(git_dir_text)) = (destination.to_str(), git_dir.to_str()) else {
debug(TAG, format!("{} or its administrative directory is not valid UTF-8, so no work item can name it", destination.display()).as_str());
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") };
};
let item: WorkItem = WorkItem {
kind: WorkKind::Trash,
repository: repository.to_string(),
git_dir: git_dir_text.to_string(),
branch_ref: String::from(TRASH_REFERENCE),
oid: manifest.name.clone(),
parent: manifest.encode(),
created: now_iso(),
tries: 0,
};
if let Err(reason) = enqueue(work_directory(git_dir).as_path(), &item) {
debug(TAG, format!("the work item could not be written: {reason}").as_str());
return Handed::Here { record: None, why: format!("the work item could not be written: {reason}") };
}
let record: PathBuf = state_directory(work_directory(git_dir).as_path(), PENDING).join(item.id());
let Some(Route { address, provision: host }) = (reach.router)(real_git, git_dir) else {
return Handed::Here { record: Some(record), why: String::from("no commit service can be started from this session") };
};
if let Some(setup) = host.as_ref()
&& let Err(failure) = (reach.provisioner)(setup)
{
return Handed::Here { record: Some(record), why: render(&failure) };
}
let config: ClientConfig = ClientConfig { address, activation: Activation::Managed, start_wait: reach.start_wait, commit_wait: COMMIT_WAIT };
return match wake(&config, git_dir, None, notices) {
Ok(()) => Handed::Service,
Err(failure) => Handed::Here { record: Some(record), why: render(&failure) },
};
}
fn is_not_empty(error: &std::io::Error) -> bool {
return matches!(error.raw_os_error(), Some(libc::ENOTEMPTY | libc::EEXIST));
}
fn verify_association(root: &File, repository: &Path, git_dir: &Path) -> Result<(), String> {
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()); })?;
if !pointer.metadata().is_ok_and(|found: std::fs::Metadata| { return found.is_file(); }) {
return Err(format!("{}/.git is not a file, so its trash is left alone", repository.display()));
}
let mut text: String = String::new();
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()); })?;
let Some(recorded) = text.strip_prefix("gitdir:").map(|rest: &str| { return rest.trim(); }) else {
return Err(format!("{}/.git does not point at an administrative directory, so its trash is left alone", repository.display()));
};
let pointed: PathBuf = if Path::new(recorded).is_absolute() { PathBuf::from(recorded) } else { repository.join(recorded) };
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; });
if !same {
return Err(format!("{}/.git points at {} and not at {}, so its trash is left alone", repository.display(), pointed.display(), git_dir.display()));
}
return Ok(());
}
pub fn delete_trash(repository: &Path, git_dir: &Path, manifest: &TrashManifest) -> Result<(), String> {
let root: File = open_root(repository).map_err(|error: std::io::Error| { return format!("{} cannot be opened as the worktree ({error})", repository.display()); })?;
verify_association(&root, repository, git_dir)?;
let device: u64 = device_of(&root).map_err(|error: std::io::Error| { return format!("{} cannot be examined ({error})", repository.display()); })?;
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})"); })?;
let mut shown: PathBuf = repository.to_path_buf();
let mut holders: Vec<File> = vec![root];
for name in &names {
shown.push(name);
let Some(held) = holders.last() else { return Err(format!("{} cannot be held", shown.display())) };
let next: File = match open_directory_at(held, name.as_os_str()) {
Ok(opened) => opened,
Err(error) if error.kind() == std::io::ErrorKind::NotFound => return Ok(()),
Err(error) => return Err(format!("{} is not a plain directory ({error}), so the trash below it is left alone", shown.display())),
};
if device_of(&next).ok() != Some(device) {
return Err(format!("{} is on another device, so the trash below it is left alone", shown.display()));
}
holders.push(next);
}
let Some(parent) = holders.last() else { return Ok(()) };
let trash: File = match open_directory_at(parent, std::ffi::OsStr::new(manifest.name.as_str())) {
Ok(opened) => opened,
Err(error) if error.kind() == std::io::ErrorKind::NotFound => return Ok(()),
Err(error) => return Err(format!("{} is not a plain directory ({error}), so it is left alone", shown.join(&manifest.name).display())),
};
let found = identity_of(&trash).map_err(|error: std::io::Error| { return format!("{} cannot be examined ({error})", shown.join(&manifest.name).display()); })?;
if found != manifest.trash {
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()));
}
let inside: PathBuf = shown.join(&manifest.name);
let left: Vec<String> = remove_entries(&EntryRemoval { inside: inside.as_path(), trash: &trash, manifest, host: &HostResources::read(), remove: &remove_tree_beneath });
if !left.is_empty() {
return Err(left.join("; "));
}
let strangers: Vec<String> = std::fs::read_dir(handle_path(&trash))
.map_err(|error: std::io::Error| { return format!("{} cannot be listed ({error})", inside.display()); })?
.flatten()
.map(|entry: std::fs::DirEntry| { return entry.file_name().to_string_lossy().into_owned(); })
.collect();
if !strangers.is_empty() {
return Err(format!("{} also holds {}, which the snapshot did not put there, so the trash directory is left alone", inside.display(), strangers.join(", ")));
}
drop(trash);
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()); })?;
if let [.., above, _] = holders.as_slice()
&& let Some(last) = names.last()
&& let Err(error) = remove_at(above, last.as_os_str(), true)
&& !is_not_empty(&error)
{
debug(TAG, format!("{} cannot be removed ({error}); it stays", shown.display()).as_str());
}
return Ok(());
}
pub fn empty_trash(item: &WorkItem) -> Verdict {
let manifest: TrashManifest = match TrashManifest::decode(item.oid.as_str(), item.parent.as_str()) {
Ok(found) => found,
Err(reason) => return Verdict::Failed(reason),
};
return match delete_trash(Path::new(item.repository.as_str()), Path::new(item.git_dir.as_str()), &manifest) {
Ok(()) => Verdict::Done,
Err(reason) => Verdict::Failed(reason),
};
}