use super::diagnostic_log::warn;
use super::push_failure_notice::{ORPHAN_NOTICE_DIRECTORY, notice_directory, record_lost_push};
use super::private_storage::{create_private_directory_all, write_private_file_unsynced};
use super::service_instance::instance_of;
use super::work_item::{CLAIMED, PENDING, WORK_DIRECTORY, WorkItem, WorkKind};
use std::path::{Path, PathBuf};
const TAG: &str = "worktreeRemovalRescue";
pub const SNAPSHOT_DIRECTORY: &str = "cli-git/removal-snapshots";
const GIT_DIR_FILE: &str = "gitdir";
const NOTICES: &str = "notices";
const WORK: &str = "work";
fn files_of(directory: &Path) -> Vec<PathBuf> {
let Ok(listing) = std::fs::read_dir(directory) else { return Vec::new() };
let mut paths: Vec<PathBuf> = listing.flatten().map(|entry| { return entry.path(); }).filter(|path| { return path.is_file(); }).collect();
paths.sort();
return paths;
}
fn name_of(path: &Path) -> String {
return path.file_name().map(|name| { return name.to_string_lossy().into_owned(); }).unwrap_or_default();
}
fn copy_into(directory: &Path, name: &str, source: &Path) -> std::io::Result<()> {
create_private_directory_all(directory)?;
return write_private_file_unsynced(directory.join(name).as_path(), std::fs::read(source)?.as_slice());
}
pub fn snapshot_linked_worktrees(common_dir: &Path) {
let Ok(listing) = std::fs::read_dir(common_dir.join("worktrees")) else { return };
for entry in listing.flatten() {
let git_dir: PathBuf = entry.path();
if !git_dir.is_dir() {
continue;
}
let notices: Vec<PathBuf> = files_of(notice_directory(git_dir.as_path()).as_path());
let work_directory: PathBuf = git_dir.join(WORK_DIRECTORY);
let pushes: Vec<(&str, PathBuf)> = [PENDING, CLAIMED]
.into_iter()
.flat_map(|state| { return files_of(work_directory.join(state).as_path()).into_iter().map(move |path| { return (state, path); }); })
.filter(|(_, path)| { return name_of(path).starts_with(format!("{}-", WorkKind::Push.name()).as_str()); })
.collect();
if notices.is_empty() && pushes.is_empty() {
continue;
}
let snapshot: PathBuf = common_dir.join(SNAPSHOT_DIRECTORY).join(instance_of(git_dir.as_path()));
let _replaced = std::fs::remove_dir_all(snapshot.as_path());
let copied: std::io::Result<()> = (|| {
create_private_directory_all(snapshot.as_path())?;
write_private_file_unsynced(snapshot.join(GIT_DIR_FILE).as_path(), git_dir.to_string_lossy().as_bytes())?;
for path in ¬ices {
copy_into(snapshot.join(NOTICES).as_path(), name_of(path).as_str(), path.as_path())?;
}
for (state, path) in &pushes {
copy_into(snapshot.join(WORK).as_path(), format!("{state}-{}", name_of(path)).as_str(), path.as_path())?;
}
return Ok(());
})();
if let Err(error) = copied {
warn(TAG, format!("snapshotting {} failed: {error}", git_dir.display()).as_str());
let _removed = std::fs::remove_dir_all(snapshot.as_path());
}
}
}
pub fn settle_removals(common_dir: &Path) {
let Ok(listing) = std::fs::read_dir(common_dir.join(SNAPSHOT_DIRECTORY)) else { return };
for entry in listing.flatten() {
let snapshot: PathBuf = entry.path();
let Ok(named) = std::fs::read_to_string(snapshot.join(GIT_DIR_FILE)) else {
continue;
};
if Path::new(named.as_str()).exists() {
let _discarded = std::fs::remove_dir_all(snapshot.as_path());
continue;
}
let orphans: PathBuf = common_dir.join(ORPHAN_NOTICE_DIRECTORY);
for path in files_of(snapshot.join(NOTICES).as_path()) {
let moved: std::io::Result<()> = create_private_directory_all(orphans.as_path()).and_then(|()| { return std::fs::rename(path.as_path(), orphans.join(name_of(path.as_path()))); });
if let Err(error) = moved {
warn(TAG, format!("keeping the notice {} of a removed worktree failed: {error}", path.display()).as_str());
}
}
for path in files_of(snapshot.join(WORK).as_path()) {
let item: Result<WorkItem, String> = std::fs::read_to_string(path.as_path()).map_err(|error| { return error.to_string(); }).and_then(|text| { return WorkItem::decode(text.as_str()); });
match item {
Ok(found) => {
if let Err(reason) = record_lost_push(common_dir, found.branch_ref.as_str(), found.oid.as_str()) {
warn(TAG, format!("recording the lost push of {} failed: {reason}", found.oid).as_str());
}
}
Err(reason) => warn(TAG, format!("a push record of a removed worktree is unreadable: {reason}").as_str()),
}
}
let _settled = std::fs::remove_dir_all(snapshot.as_path());
}
}
#[cfg(test)]
#[path = "worktree_removal_rescue_tests.rs"]
mod tests;