use std::collections::{BTreeMap, BTreeSet};
use std::path::{Path, PathBuf};
use serde::Serialize;
use super::fold::LedgerState;
use super::{EventKind, LedgerAppender, LedgerError, LedgerEvent, WorktreeLedger};
use crate::session_manager::worktree_registry::probe_registered_worktrees;
#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
pub struct UnlistableRepo {
pub repo: PathBuf,
pub reason: String,
}
#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize)]
pub struct ReconcileReport {
pub removed: usize,
pub still_listed: usize,
pub unknown: usize,
pub unlistable: Vec<UnlistableRepo>,
}
impl ReconcileReport {
pub fn missing(&self) -> usize {
self.still_listed + self.unknown
}
}
pub fn reconcile_removed(
ledger: &WorktreeLedger,
state: &LedgerState,
) -> Result<ReconcileReport, LedgerError> {
let mut gone: BTreeMap<&Path, Vec<&Path>> = BTreeMap::new();
for wt in state.live.values() {
if directory_gone(&wt.path) {
gone.entry(wt.repo.as_path()).or_default().push(&wt.path);
}
}
let mut report = ReconcileReport::default();
let mut appender: Option<LedgerAppender> = None;
for (repo, trees) in gone {
let listed = match listed_keys(repo) {
Ok(listed) => listed,
Err(reason) => {
report.unknown += trees.len();
report.unlistable.push(UnlistableRepo {
repo: repo.to_path_buf(),
reason,
});
continue;
}
};
for path in trees {
if listed.contains(path) || listed.contains(&lenient_key(path)) {
report.still_listed += 1;
continue;
}
if appender.is_none() {
appender = Some(ledger.open_appender()?);
}
if let Some(out) = appender.as_mut() {
out.append(&LedgerEvent::now(path.to_path_buf(), EventKind::Removed))?;
report.removed += 1;
}
}
}
Ok(report)
}
fn directory_gone(path: &Path) -> bool {
matches!(std::fs::symlink_metadata(path), Err(e) if e.kind() == std::io::ErrorKind::NotFound)
}
fn listed_keys(repo: &Path) -> Result<BTreeSet<PathBuf>, String> {
if directory_gone(repo) {
return Err("repository directory is gone".to_string());
}
let listed =
probe_registered_worktrees(repo).map_err(|e| format!("`git worktree list` failed: {e}"))?;
let mut keys = BTreeSet::new();
for wt in &listed {
keys.insert(wt.path.clone());
keys.insert(lenient_key(&wt.path));
}
if !keys.contains(repo) && !keys.contains(&lenient_key(repo)) {
return Err("`git worktree list` does not list the repository itself".to_string());
}
Ok(keys)
}
fn lenient_key(path: &Path) -> PathBuf {
let mut rest = Vec::new();
let mut cur = path;
loop {
if let Ok(canon) = std::fs::canonicalize(cur) {
return rest.iter().rev().fold(canon, |acc, name| acc.join(name));
}
match (cur.parent(), cur.file_name()) {
(Some(parent), Some(name)) => {
rest.push(name.to_os_string());
cur = parent;
}
_ => return path.to_path_buf(),
}
}
}