use std::path::{Path, PathBuf};
use std::time::Duration;
type R<T> = Result<T, Box<dyn std::error::Error>>;
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum Lock {
Absent,
Held(Vec<u32>),
Stale,
Unknown(String),
}
pub fn index_lock(gd: &Path) -> PathBuf {
gd.join("index.lock")
}
pub fn lock_state(gd: &Path) -> Lock {
let path = index_lock(gd);
let id = |p: &Path| {
std::fs::symlink_metadata(p)
.ok()
.map(|m| (m.len(), m.modified().ok()))
};
let Some(first) = id(&path) else {
return Lock::Absent;
};
for look in 0..3 {
if look > 0 {
std::thread::sleep(Duration::from_millis(150));
match id(&path) {
None => return Lock::Absent,
Some(now) if now != first => return Lock::Held(Vec::new()),
Some(_) => {}
}
}
match holders(&path) {
Ok(pids) if pids.is_empty() => {}
Ok(pids) => return Lock::Held(pids),
Err(why) => return Lock::Unknown(why),
}
}
Lock::Stale
}
fn holders(path: &Path) -> Result<Vec<u32>, String> {
let want = std::fs::canonicalize(path).unwrap_or_else(|_| path.to_path_buf());
if Path::new("/proc/self/fd").is_dir() {
let mut pids = Vec::new();
let procs = std::fs::read_dir("/proc").map_err(|e| format!("/proc: {e}"))?;
for p in procs.flatten() {
let Some(pid) = p.file_name().to_str().and_then(|s| s.parse::<u32>().ok()) else {
continue;
};
let Ok(fds) = std::fs::read_dir(p.path().join("fd")) else {
continue;
};
for fd in fds.flatten() {
if let Ok(target) = std::fs::read_link(fd.path()) {
if target == want || target == path {
pids.push(pid);
break;
}
}
}
}
return Ok(pids);
}
let out = std::process::Command::new("lsof")
.arg("-t")
.arg("--")
.arg(&want)
.output()
.map_err(|e| format!("could not run lsof to see who holds it: {e}"))?;
let text = String::from_utf8_lossy(&out.stdout);
let pids: Vec<u32> = text
.split_whitespace()
.filter_map(|s| s.parse().ok())
.collect();
match (out.status.code(), pids.is_empty()) {
(_, false) => Ok(pids),
(Some(1), true) => Ok(Vec::new()),
(Some(0), true) => Ok(Vec::new()),
_ => Err(format!(
"lsof failed: {}",
String::from_utf8_lossy(&out.stderr).trim()
)),
}
}
pub fn clear_stale_lock(gd: &Path, out: &mut dyn std::io::Write) -> R<Option<String>> {
let path = index_lock(gd);
match lock_state(gd) {
Lock::Absent => Ok(None),
Lock::Stale => {
std::fs::remove_file(&path).or_else(|e| match e.kind() {
std::io::ErrorKind::NotFound => Ok(()),
_ => Err(e),
})?;
writeln!(
out,
"land: removed a stale {} (no running process has it open; a git process that \
was killed left it behind).",
path.display()
)?;
Ok(None)
}
Lock::Held(pids) => Ok(Some(format!(
"{} is held by a running process{}; another git command is using this repository",
path.display(),
if pids.is_empty() {
String::new()
} else {
format!(
" (pid {})",
pids.iter()
.map(|p| p.to_string())
.collect::<Vec<_>>()
.join(", ")
)
}
))),
Lock::Unknown(why) => Ok(Some(format!(
"{} exists and weave cannot tell whether a running process holds it ({why})",
path.display()
))),
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Record {
pub state: String,
pub pre: String,
pub tip: String,
}
fn record_path(gd: &Path) -> PathBuf {
gd.join("weave-land-merge")
}
pub fn read_record(gd: &Path) -> Option<Record> {
let text = std::fs::read_to_string(record_path(gd)).ok()?;
let f: Vec<&str> = text.split_whitespace().collect();
match f.as_slice() {
[state, pre, tip] => Some(Record {
state: state.to_string(),
pre: pre.to_string(),
tip: tip.to_string(),
}),
_ => None,
}
}
pub fn write_record(gd: &Path, state: &str, pre: &str, tip: &str) -> R<()> {
let tmp = gd.join("weave-land-merge.tmp");
std::fs::write(&tmp, format!("{state} {pre} {tip}\n"))?;
std::fs::rename(&tmp, record_path(gd))?;
Ok(())
}
pub fn clear_record(gd: &Path) {
let _ = std::fs::remove_file(record_path(gd));
}
pub fn merge_not_written(stdout: &str, stderr: &str) -> bool {
[stdout, stderr].iter().any(|t| {
t.contains("index.lock")
|| t.contains("Unable to write index")
|| t.contains("unable to write")
|| t.lines().any(|l| l.starts_with("fatal:"))
})
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn a_lock_nobody_has_open_is_stale_and_one_held_open_is_not() {
let gd = std::env::temp_dir().join(format!("weave-lock-{}", std::process::id()));
std::fs::create_dir_all(&gd).unwrap();
assert_eq!(lock_state(&gd), Lock::Absent);
std::fs::write(index_lock(&gd), "").unwrap();
let held = std::fs::File::open(index_lock(&gd)).unwrap();
match lock_state(&gd) {
Lock::Held(pids) => assert!(pids.contains(&std::process::id()), "{pids:?}"),
Lock::Unknown(_) => {} other => panic!("an open lock is not stale: {other:?}"),
}
drop(held);
match lock_state(&gd) {
Lock::Stale | Lock::Unknown(_) => {}
other => panic!("{other:?}"),
}
let _ = std::fs::remove_dir_all(&gd);
}
#[test]
fn git_saying_it_could_not_write_the_index_is_not_a_merge() {
assert!(merge_not_written(
"Automatic merge failed; fix conflicts and then commit the result.\n",
"error: Unable to write index.\n"
));
assert!(!merge_not_written(
"Auto-merging a.go\nCONFLICT (content): Merge conflict in a.go\n",
""
));
}
}