use std::{path::Path, time::Duration};
use tokio::time::sleep;
const TIMEOUT_MS:u64 = 30_000;
const POLL_INITIAL_MS:u64 = 50;
const POLL_MAX_MS:u64 = 500;
const STALE_AGE_SECS:u64 = 30;
pub async fn Fn(Directory:&str) -> bool {
Inner(Directory, TIMEOUT_MS, POLL_INITIAL_MS, STALE_AGE_SECS).await
}
async fn Inner(
Directory:&str,
TimeoutMs:u64,
PollInitialMs:u64,
StaleAgeSecs:u64,
) -> bool {
let LockPath = Path::new(Directory).join(".git").join("index.lock");
if !LockPath.exists() {
return true;
}
if let Ok(Metadata) = std::fs::metadata(&LockPath)
&& let Ok(Modified) = Metadata.modified()
&& let Ok(Age) = Modified.elapsed()
&& Age.as_secs() >= StaleAgeSecs
{
eprintln!(
"Removing stale git index lock in '{}' (age: {}s).",
Directory,
Age.as_secs()
);
if let Err(Error) = std::fs::remove_file(&LockPath) {
eprintln!("Failed to remove stale index lock in '{}': {}", Directory, Error);
}
return !LockPath.exists();
}
let mut Elapsed:u64 = 0;
let mut Interval:u64 = PollInitialMs;
loop {
if Elapsed >= TimeoutMs {
eprintln!(
"Timed out waiting {}ms for git index lock in '{}'.",
TimeoutMs, Directory
);
return false;
}
sleep(Duration::from_millis(Interval)).await;
Elapsed += Interval;
Interval = (Interval * 2).min(POLL_MAX_MS);
if !LockPath.exists() {
return true;
}
}
}
#[cfg(test)]
mod Tests {
use std::{fs, path::PathBuf};
use super::Inner;
fn TempGitDir(Label:&str) -> PathBuf {
let Dir = std::env::temp_dir()
.join(format!("prun_index_lock_test_{}_{}", Label, std::process::id()));
fs::create_dir_all(Dir.join(".git")).expect("create temp .git dir");
Dir
}
fn Cleanup(Dir:&PathBuf) { let _ = fs::remove_dir_all(Dir); }
#[tokio::test]
async fn Returns_True_When_No_Lock_File_Exists() {
let Dir = TempGitDir("no_lock");
let Result = Inner(&Dir.to_string_lossy(), 500, 10, 0).await;
Cleanup(&Dir);
assert!(Result);
}
#[tokio::test]
async fn Returns_True_When_Lock_Released_Before_Timeout() {
let Dir = TempGitDir("released");
let LockFile = Dir.join(".git").join("index.lock");
fs::write(&LockFile, b"").expect("write lock file");
let LockFileClone = LockFile.clone();
tokio::spawn(async move {
tokio::time::sleep(std::time::Duration::from_millis(80)).await;
let _ = fs::remove_file(LockFileClone);
});
let Result = Inner(&Dir.to_string_lossy(), 2_000, 10, 30).await;
Cleanup(&Dir);
assert!(Result);
}
#[tokio::test]
async fn Returns_False_When_Lock_Never_Released() {
let Dir = TempGitDir("timeout");
let LockFile = Dir.join(".git").join("index.lock");
fs::write(&LockFile, b"").expect("write lock file");
let Result = Inner(&Dir.to_string_lossy(), 150, 10, 30).await;
Cleanup(&Dir);
assert!(!Result);
}
#[tokio::test]
async fn Removes_Stale_Lock_And_Returns_True() {
let Dir = TempGitDir("stale");
let LockFile = Dir.join(".git").join("index.lock");
fs::write(&LockFile, b"").expect("write lock file");
let Result = Inner(&Dir.to_string_lossy(), 500, 10, 0).await;
assert!(Result);
assert!(!LockFile.exists(), "stale lock should have been removed by Inner");
Cleanup(&Dir);
}
#[tokio::test]
async fn Returns_True_For_Missing_Git_Dir() {
let Dir = std::env::temp_dir()
.join(format!("prun_index_lock_test_no_git_{}", std::process::id()));
fs::create_dir_all(&Dir).expect("create dir");
let Result = Inner(&Dir.to_string_lossy(), 500, 10, 0).await;
let _ = fs::remove_dir_all(&Dir);
assert!(Result);
}
}