use fs2::FileExt;
use std::fs::{self, File, OpenOptions};
use std::io;
use std::path::{Path, PathBuf};
use crate::git;
pub struct PlanrLock {
_file: File,
}
impl PlanrLock {
pub fn shared(cwd: &Path) -> io::Result<Self> {
let path = lock_path(cwd)?;
let file = open_lock_file(&path)?;
file.lock_shared()?;
Ok(PlanrLock { _file: file })
}
pub fn exclusive(cwd: &Path) -> io::Result<Self> {
let path = lock_path(cwd)?;
let file = open_lock_file(&path)?;
file.lock_exclusive()?;
Ok(PlanrLock { _file: file })
}
}
fn lock_path(cwd: &Path) -> io::Result<PathBuf> {
let gd =
git::git_common_dir(cwd).map_err(|e| io::Error::other(format!("git-common-dir: {e}")))?;
Ok(Path::new(&gd).join("planr.lock"))
}
fn open_lock_file(path: &Path) -> io::Result<File> {
if let Some(parent) = path.parent() {
fs::create_dir_all(parent)?;
}
OpenOptions::new()
.create(true)
.truncate(true)
.read(true)
.write(true)
.open(path)
}
#[cfg(test)]
mod tests {
use super::*;
use std::fs;
use std::sync::{Arc, Barrier};
use std::thread;
use tempfile::TempDir;
fn init_repo() -> (TempDir, PathBuf) {
let tmp = TempDir::new().unwrap();
let repo = tmp.path().join("repo");
fs::create_dir_all(&repo).unwrap();
let out = std::process::Command::new("git")
.args(["init", "-b", "main"])
.current_dir(&repo)
.output()
.unwrap();
assert!(out.status.success());
std::process::Command::new("git")
.args(["config", "user.email", "test@test"])
.current_dir(&repo)
.output()
.unwrap();
std::process::Command::new("git")
.args(["config", "user.name", "Test"])
.current_dir(&repo)
.output()
.unwrap();
std::process::Command::new("git")
.args(["commit", "--allow-empty", "-m", "init"])
.current_dir(&repo)
.output()
.unwrap();
(tmp, repo)
}
#[test]
fn test_lock_path_matches_planr_lock() {
let (_tmp, repo) = init_repo();
let path = lock_path(&repo).unwrap();
assert_eq!(path.file_name().unwrap(), "planr.lock");
assert!(path.to_string_lossy().contains(".git"));
}
#[test]
fn test_shared_lock_does_not_block_shared() {
let (_tmp, repo) = init_repo();
let lock1 = PlanrLock::shared(&repo).unwrap();
let lock2 = PlanrLock::shared(&repo).unwrap();
drop(lock1);
drop(lock2);
}
#[test]
fn test_exclusive_lock_serializes() {
let (_tmp, repo) = init_repo();
let repo1 = repo.clone();
let repo2 = repo.clone();
let barrier = Arc::new(Barrier::new(2));
let b1 = barrier.clone();
let t1 = thread::spawn(move || {
let l = PlanrLock::exclusive(&repo1).unwrap();
b1.wait(); thread::sleep(std::time::Duration::from_millis(50));
drop(l);
});
let b2 = barrier;
let t2 = thread::spawn(move || {
b2.wait(); let started = std::time::Instant::now();
let l = PlanrLock::exclusive(&repo2).unwrap();
let elapsed = started.elapsed();
assert!(
elapsed >= std::time::Duration::from_millis(40),
"exclusive lock should block: elapsed={elapsed:?}"
);
drop(l);
});
t1.join().unwrap();
t2.join().unwrap();
}
}