git_stk/lock.rs
1//! A coarse advisory lock so two git-stk processes never run state-mutating
2//! commands at once. Git locks its own index and refs, but not git-stk's
3//! multi-step orchestration (snapshot, rebases, metadata, provider calls), so
4//! a concurrent run could clobber the undo snapshot or half-rewrite the stack.
5
6use std::fs;
7use std::io::{ErrorKind, Write};
8use std::path::PathBuf;
9
10use anyhow::{Context, Result, bail};
11
12use crate::git;
13
14const LOCK_FILE: &str = "stk-lock";
15
16/// Held for the duration of a mutating command; removes the lock file on drop.
17/// Outside a git repo it is a no-op, so the command surfaces its own error.
18pub struct Lock {
19 path: Option<PathBuf>,
20}
21
22impl Lock {
23 /// Take the lock for `command`, or fail if another git-stk process holds
24 /// it. Naming the command makes the contention message actionable.
25 pub fn acquire(command: &str) -> Result<Self> {
26 let Ok(path) = git::git_path(LOCK_FILE) else {
27 // Not a git repo: nothing to guard, and the command will report
28 // the real problem itself.
29 return Ok(Self { path: None });
30 };
31 let path = PathBuf::from(path);
32
33 match fs::OpenOptions::new()
34 .write(true)
35 .create_new(true)
36 .open(&path)
37 {
38 Ok(mut file) => {
39 // Best effort: the holder line only feeds the error message.
40 let _ = writeln!(file, "{} {command}", std::process::id());
41 Ok(Self { path: Some(path) })
42 }
43 Err(error) if error.kind() == ErrorKind::AlreadyExists => {
44 let holder = fs::read_to_string(&path).unwrap_or_default();
45 let holder = holder.trim();
46 let by = if holder.is_empty() {
47 String::new()
48 } else {
49 format!(" ({holder})")
50 };
51 bail!(
52 "another git stk operation is in progress{by}; wait for it to \
53 finish, or remove {} if it is stale",
54 path.display()
55 );
56 }
57 Err(error) => {
58 Err(error).with_context(|| format!("failed to take the lock at {}", path.display()))
59 }
60 }
61 }
62}
63
64impl Drop for Lock {
65 fn drop(&mut self) {
66 if let Some(path) = &self.path {
67 let _ = fs::remove_file(path);
68 }
69 }
70}