1use std::path::{Path, PathBuf};
23use std::time::Duration;
24
25type R<T> = Result<T, Box<dyn std::error::Error>>;
26
27#[derive(Debug, Clone, PartialEq, Eq)]
29pub enum Lock {
30 Absent,
31 Held(Vec<u32>),
33 Stale,
35 Unknown(String),
37}
38
39pub fn index_lock(gd: &Path) -> PathBuf {
41 gd.join("index.lock")
42}
43
44pub fn lock_state(gd: &Path) -> Lock {
47 let path = index_lock(gd);
48 let id = |p: &Path| {
49 std::fs::symlink_metadata(p)
50 .ok()
51 .map(|m| (m.len(), m.modified().ok()))
52 };
53 let Some(first) = id(&path) else {
54 return Lock::Absent;
55 };
56 for look in 0..3 {
57 if look > 0 {
58 std::thread::sleep(Duration::from_millis(150));
59 match id(&path) {
60 None => return Lock::Absent,
61 Some(now) if now != first => return Lock::Held(Vec::new()),
62 Some(_) => {}
63 }
64 }
65 match holders(&path) {
66 Ok(pids) if pids.is_empty() => {}
67 Ok(pids) => return Lock::Held(pids),
68 Err(why) => return Lock::Unknown(why),
69 }
70 }
71 Lock::Stale
72}
73
74fn holders(path: &Path) -> Result<Vec<u32>, String> {
76 let want = std::fs::canonicalize(path).unwrap_or_else(|_| path.to_path_buf());
77 if Path::new("/proc/self/fd").is_dir() {
78 let mut pids = Vec::new();
79 let procs = std::fs::read_dir("/proc").map_err(|e| format!("/proc: {e}"))?;
80 for p in procs.flatten() {
81 let Some(pid) = p.file_name().to_str().and_then(|s| s.parse::<u32>().ok()) else {
82 continue;
83 };
84 let Ok(fds) = std::fs::read_dir(p.path().join("fd")) else {
87 continue;
88 };
89 for fd in fds.flatten() {
90 if let Ok(target) = std::fs::read_link(fd.path()) {
91 if target == want || target == path {
92 pids.push(pid);
93 break;
94 }
95 }
96 }
97 }
98 return Ok(pids);
99 }
100 let out = std::process::Command::new("lsof")
101 .arg("-t")
102 .arg("--")
103 .arg(&want)
104 .output()
105 .map_err(|e| format!("could not run lsof to see who holds it: {e}"))?;
106 let text = String::from_utf8_lossy(&out.stdout);
107 let pids: Vec<u32> = text
108 .split_whitespace()
109 .filter_map(|s| s.parse().ok())
110 .collect();
111 match (out.status.code(), pids.is_empty()) {
112 (_, false) => Ok(pids),
113 (Some(1), true) => Ok(Vec::new()),
114 (Some(0), true) => Ok(Vec::new()),
115 _ => Err(format!(
116 "lsof failed: {}",
117 String::from_utf8_lossy(&out.stderr).trim()
118 )),
119 }
120}
121
122pub fn clear_stale_lock(gd: &Path, out: &mut dyn std::io::Write) -> R<Option<String>> {
126 let path = index_lock(gd);
127 match lock_state(gd) {
128 Lock::Absent => Ok(None),
129 Lock::Stale => {
130 std::fs::remove_file(&path).or_else(|e| match e.kind() {
131 std::io::ErrorKind::NotFound => Ok(()),
132 _ => Err(e),
133 })?;
134 writeln!(
135 out,
136 "land: removed a stale {} (no running process has it open; a git process that \
137 was killed left it behind).",
138 path.display()
139 )?;
140 Ok(None)
141 }
142 Lock::Held(pids) => Ok(Some(format!(
143 "{} is held by a running process{}; another git command is using this repository",
144 path.display(),
145 if pids.is_empty() {
146 String::new()
147 } else {
148 format!(
149 " (pid {})",
150 pids.iter()
151 .map(|p| p.to_string())
152 .collect::<Vec<_>>()
153 .join(", ")
154 )
155 }
156 ))),
157 Lock::Unknown(why) => Ok(Some(format!(
158 "{} exists and weave cannot tell whether a running process holds it ({why})",
159 path.display()
160 ))),
161 }
162}
163
164#[derive(Debug, Clone, PartialEq, Eq)]
166pub struct Record {
167 pub state: String,
170 pub pre: String,
172 pub tip: String,
174}
175
176fn record_path(gd: &Path) -> PathBuf {
177 gd.join("weave-land-merge")
178}
179
180pub fn read_record(gd: &Path) -> Option<Record> {
181 let text = std::fs::read_to_string(record_path(gd)).ok()?;
182 let f: Vec<&str> = text.split_whitespace().collect();
183 match f.as_slice() {
184 [state, pre, tip] => Some(Record {
185 state: state.to_string(),
186 pre: pre.to_string(),
187 tip: tip.to_string(),
188 }),
189 _ => None,
190 }
191}
192
193pub fn write_record(gd: &Path, state: &str, pre: &str, tip: &str) -> R<()> {
194 let tmp = gd.join("weave-land-merge.tmp");
195 std::fs::write(&tmp, format!("{state} {pre} {tip}\n"))?;
196 std::fs::rename(&tmp, record_path(gd))?;
197 Ok(())
198}
199
200pub fn clear_record(gd: &Path) {
201 let _ = std::fs::remove_file(record_path(gd));
202}
203
204pub fn merge_not_written(stdout: &str, stderr: &str) -> bool {
208 [stdout, stderr].iter().any(|t| {
209 t.contains("index.lock")
210 || t.contains("Unable to write index")
211 || t.contains("unable to write")
212 || t.lines().any(|l| l.starts_with("fatal:"))
213 })
214}
215
216#[cfg(test)]
217mod tests {
218 use super::*;
219
220 #[test]
221 fn a_lock_nobody_has_open_is_stale_and_one_held_open_is_not() {
222 let gd = std::env::temp_dir().join(format!("weave-lock-{}", std::process::id()));
223 std::fs::create_dir_all(&gd).unwrap();
224 assert_eq!(lock_state(&gd), Lock::Absent);
225 std::fs::write(index_lock(&gd), "").unwrap();
226 let held = std::fs::File::open(index_lock(&gd)).unwrap();
227 match lock_state(&gd) {
228 Lock::Held(pids) => assert!(pids.contains(&std::process::id()), "{pids:?}"),
229 Lock::Unknown(_) => {} other => panic!("an open lock is not stale: {other:?}"),
231 }
232 drop(held);
233 match lock_state(&gd) {
234 Lock::Stale | Lock::Unknown(_) => {}
235 other => panic!("{other:?}"),
236 }
237 let _ = std::fs::remove_dir_all(&gd);
238 }
239
240 #[test]
241 fn git_saying_it_could_not_write_the_index_is_not_a_merge() {
242 assert!(merge_not_written(
243 "Automatic merge failed; fix conflicts and then commit the result.\n",
244 "error: Unable to write index.\n"
245 ));
246 assert!(!merge_not_written(
247 "Auto-merging a.go\nCONFLICT (content): Merge conflict in a.go\n",
248 ""
249 ));
250 }
251}