git-slop 0.16.4

A deterministic repository token-defragmenter for humans and AI agents.
Documentation
use std::fs::{self, File, OpenOptions};
use std::path::{Path, PathBuf};

use anyhow::{Context, Result};
use fs2::FileExt;
use serde_json::json;

use crate::error::{ClassifiedError, ErrorKind};

#[derive(Debug)]
pub struct ScanLock {
    file: File,
    owner_path: PathBuf,
}

impl Drop for ScanLock {
    fn drop(&mut self) {
        let _ = fs::remove_file(&self.owner_path);
        let _ = FileExt::unlock(&self.file);
    }
}

pub fn acquire_scan_lock(state_root: &Path) -> Result<ScanLock> {
    fs::create_dir_all(state_root).with_context(|| {
        format!(
            "failed to create mutable scan-state directory {}",
            state_root.display()
        )
    })?;
    let path = state_root.join("scan.lock");
    let owner_path = state_root.join("scan.lock.owner");
    let file = OpenOptions::new()
        .create(true)
        .truncate(false)
        .read(true)
        .write(true)
        .open(&path)
        .with_context(|| format!("failed to open scan lock {}", path.display()))?;
    if file.try_lock_exclusive().is_err() {
        let owner = fs::read_to_string(&owner_path).unwrap_or_default();
        let pid = owner
            .trim()
            .strip_prefix("pid=")
            .and_then(|value| value.parse::<u32>().ok());
        return Err(ClassifiedError::new(
            ErrorKind::ResourceLimit,
            "scan_locked",
            format!("another git-slop scan owns {}", path.display()),
        )
        .at("/state_dir/scan.lock")
        .with_details(json!({
            "lock_path": path,
            "owner_pid": pid,
            "retry_guidance": "wait for the owning scan or choose a distinct --state-dir"
        }))
        .into());
    }
    fs::write(&owner_path, format!("pid={}\n", std::process::id())).with_context(|| {
        format!(
            "failed to write scan lock owner metadata {}",
            owner_path.display()
        )
    })?;
    Ok(ScanLock { file, owner_path })
}