Skip to main content

spec_driven_docs/landing/
lock.rs

1//! The one lock a target takes while it is being written.
2
3use camino::{Utf8Path, Utf8PathBuf};
4
5use crate::domain::ownership::Sha256;
6use crate::domain::paths::UserEnv;
7use crate::error::AppError;
8use crate::transaction::lock::Lock;
9
10/// Where this tool keeps state that outlives a command.
11///
12/// # Errors
13///
14/// [`AppError::Usage`] when no state root resolves.
15pub fn state_root() -> Result<Utf8PathBuf, AppError> {
16    Ok(UserEnv::from_process()
17        .state_root()
18        .ok_or_else(|| AppError::Usage("no state root resolves".to_string()))?
19        .path)
20}
21
22/// The lock one target takes, keyed by where it is.
23///
24/// A digest rather than the path itself: a lock file named after a
25/// repository would put a person's directory layout in the state root, and
26/// two targets whose paths differ only in case would collide.
27///
28/// # Errors
29///
30/// [`AppError::Usage`] when no state root resolves.
31pub fn path_for(target: &Utf8Path) -> Result<Utf8PathBuf, AppError> {
32    let key = Sha256::of(target.as_str().as_bytes());
33    Ok(state_root()?.join("locks").join(format!("{key}.lock")))
34}
35
36/// Hold one target exclusively for the whole of a landing.
37///
38/// # Errors
39///
40/// [`AppError::Busy`] when another process holds it, and I/O errors
41/// creating the lock file.
42pub fn hold(target: &Utf8Path) -> Result<Lock, AppError> {
43    Lock::exclusive(&path_for(target)?, "landing")
44}
45
46#[cfg(test)]
47mod tests {
48    #![allow(
49        clippy::unwrap_used,
50        reason = "a test panics as its failure signal, not as control flow"
51    )]
52
53    use super::*;
54
55    #[test]
56    fn two_targets_take_two_locks_and_neither_names_a_directory() {
57        let one = path_for(Utf8Path::new("/work/one")).unwrap();
58        let two = path_for(Utf8Path::new("/work/two")).unwrap();
59        assert_ne!(one, two);
60        assert!(!one.as_str().contains("work"), "{one}");
61    }
62}