use super::diagnostic_log::{debug, warn};
use super::fs_btrfs_ioctl::subvolume_destroy_at;
use super::fs_kind::is_subvolume_root;
use super::fs_mount_options::read_mount_facts;
use super::transaction_git::GitContext;
use super::worktree_clean::check_clean;
use super::worktree_remove_plan::RemovePlan;
use std::ffi::OsStr;
use std::os::unix::ffi::OsStrExt;
use std::path::{Path, PathBuf};
const TAG: &str = "worktreeRemove";
const REMOVAL_OPTION: &str = "user_subvol_rm_allowed";
fn registered_admin_directory(target: &Path) -> Option<PathBuf> {
let pointer: Vec<u8> = std::fs::read(target.join(".git")).ok()?;
let line: &[u8] = pointer.strip_suffix(b"\n").unwrap_or(pointer.as_slice());
let named: &OsStr = OsStr::from_bytes(line.strip_prefix(b"gitdir: ")?);
let admin: PathBuf = std::fs::canonicalize(target.join(named)).ok()?;
if admin.parent()?.file_name()? != "worktrees" {
return None;
}
let recorded: Vec<u8> = std::fs::read(admin.join("gitdir")).ok()?;
let recorded_path: PathBuf = PathBuf::from(OsStr::from_bytes(recorded.strip_suffix(b"\n").unwrap_or(recorded.as_slice())));
return (recorded_path == target.join(".git")).then_some(admin);
}
pub fn remove_subvolume_worktree(context: &GitContext, plan: &RemovePlan) -> bool {
let target: &Path = plan.target.as_path();
let Some(admin) = registered_admin_directory(target) else {
debug(TAG, format!("{} is not a registered linked worktree; Git removes it", target.display()).as_str());
return false;
};
if admin.join("locked").symlink_metadata().is_ok() {
debug(TAG, format!("{} is locked; Git decides", target.display()).as_str());
return false;
}
if !is_subvolume_root(target).unwrap_or(false) {
debug(TAG, format!("{} is not a subvolume; Git removes it file by file", target.display()).as_str());
return false;
}
let allowed: bool = read_mount_facts(target).ok().flatten().is_some_and(|facts| { return facts.has_option(REMOVAL_OPTION); });
if !allowed {
debug(TAG, format!("the mount of {} lacks {REMOVAL_OPTION}; Git removes the worktree file by file", target.display()).as_str());
return false;
}
let Some(common_dir) = admin.parent().and_then(Path::parent) else { return false };
if let Err(reason) = check_clean(context, target, admin.as_path(), common_dir) {
debug(TAG, format!("{} is not proven clean ({reason}); Git decides", target.display()).as_str());
return false;
}
if let Err(error) = subvolume_destroy_at(target) {
debug(TAG, format!("the subvolume of {} was not destroyed ({error}); Git removes it", target.display()).as_str());
return false;
}
if let Err(error) = std::fs::remove_dir_all(admin.as_path()) {
warn(TAG, format!("{} was removed, but its administrative directory {} was not ({error}); `git worktree prune` removes it", target.display(), admin.display()).as_str());
}
debug(TAG, format!("destroyed the subvolume {} and its administrative directory", target.display()).as_str());
return true;
}
#[cfg(test)]
#[path = "worktree_remove_tests.rs"]
mod tests;