use std::collections::HashMap;
use std::path::{Path, PathBuf};
use std::sync::{Arc, Mutex, MutexGuard};
#[derive(Default)]
pub struct CommitLocks {
locks: Mutex<HashMap<PathBuf, Arc<Mutex<()>>>>,
}
impl CommitLocks {
pub fn new() -> Self {
Self::default()
}
pub(crate) fn mutex_for(&self, key: &Path) -> Arc<Mutex<()>> {
let key = key.canonicalize().unwrap_or_else(|_| key.to_path_buf());
let mut map = self
.locks
.lock()
.unwrap_or_else(|poisoned| poisoned.into_inner());
map.entry(key)
.or_insert_with(|| Arc::new(Mutex::new(())))
.clone()
}
}
pub(crate) fn lock_recover(mutex: &Mutex<()>) -> MutexGuard<'_, ()> {
mutex
.lock()
.unwrap_or_else(|poisoned| poisoned.into_inner())
}
#[cfg(test)]
mod tests {
use super::*;
use tempfile::TempDir;
#[test]
fn same_key_shares_mutex_distinct_keys_differ() {
let a = TempDir::new().unwrap();
let b = TempDir::new().unwrap();
let locks = CommitLocks::new();
let a1 = locks.mutex_for(a.path());
let a2 = locks.mutex_for(a.path());
let b1 = locks.mutex_for(b.path());
assert!(Arc::ptr_eq(&a1, &a2), "same worktree -> same mutex");
assert!(
!Arc::ptr_eq(&a1, &b1),
"distinct worktrees -> distinct mutexes"
);
}
#[test]
fn mutex_provides_mutual_exclusion() {
let dir = TempDir::new().unwrap();
let locks = CommitLocks::new();
let m = locks.mutex_for(dir.path());
let held = lock_recover(&m);
assert!(m.try_lock().is_err(), "a held commit lock blocks re-entry");
drop(held);
assert!(m.try_lock().is_ok(), "released lock is re-acquirable");
}
#[test]
fn lock_recover_survives_poison() {
let dir = TempDir::new().unwrap();
let locks = CommitLocks::new();
let m = locks.mutex_for(dir.path());
let m2 = Arc::clone(&m);
let _ = std::thread::spawn(move || {
let _g = m2.lock().unwrap();
panic!("poison");
})
.join();
let _g = lock_recover(&m);
}
}