Skip to main content

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}