use std::io;
use std::path::PathBuf;
use crate::Repository;
#[must_use = "dropping the guard releases the maintenance lock"]
pub struct MaintenanceLock {
path: PathBuf,
file: Option<crate::locks::SharedFile>,
registration: crate::locks::Guard,
}
impl std::fmt::Debug for MaintenanceLock {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("MaintenanceLock")
.field("path", &self.path)
.finish_non_exhaustive()
}
}
impl MaintenanceLock {
pub fn release(mut self) -> Result<(), LockError> {
let _operation = crate::locks::operation();
self.registration.check_path().map_err(LockError::Io)?;
drop(self.file.take());
std::fs::remove_file(&self.path).map_err(LockError::Io)
}
}
impl Drop for MaintenanceLock {
fn drop(&mut self) {
let _operation = crate::locks::operation();
if self.file.is_some() && self.registration.check_path().is_ok() {
drop(self.file.take());
let _ = std::fs::remove_file(&self.path);
}
}
}
#[derive(Debug, thiserror::Error)]
#[non_exhaustive]
pub enum LockError {
#[error("another process is running maintenance")]
Busy,
#[error("cannot lock repository maintenance")]
Io(#[source] io::Error),
}
impl Repository {
pub fn try_lock_maintenance(&self) -> Result<MaintenanceLock, LockError> {
MaintenanceLock::acquire(self.object_dir().join("maintenance.lock"))
}
}
impl MaintenanceLock {
pub(super) fn acquire(path: PathBuf) -> Result<Self, LockError> {
let _operation = crate::locks::operation();
let file = crate::locks::unreplaceable_lock_options()
.open(&path)
.map_err(|error| {
if error.kind() == io::ErrorKind::AlreadyExists {
LockError::Busy
} else {
LockError::Io(error)
}
})?;
Self::from_file(path, file)
}
pub(super) fn from_file(path: PathBuf, file: std::fs::File) -> Result<Self, LockError> {
let _operation = crate::locks::operation();
let file = crate::locks::SharedFile::from(file);
let identity = crate::locks::capture_identity(&path, &file).map_err(LockError::Io)?;
let registration = crate::locks::Guard::new(&path, &file, identity);
Ok(MaintenanceLock {
path,
file: Some(file),
registration,
})
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn git_skips_task_while_girt_holds_lock() {
let directory = tempfile::tempdir().unwrap();
let repo = Repository::init(
crate::ObjectFormat::Sha1,
directory.path().join("r"),
crate::InitKind::Bare,
)
.unwrap();
let trace = directory.path().join("trace.json");
let mut guard = Some(repo.try_lock_maintenance().unwrap());
for held in [true, false] {
let mut command = std::process::Command::new("git");
for (key, _) in std::env::vars_os() {
if key.to_string_lossy().starts_with("GIT_") {
command.env_remove(key);
}
}
let output = command
.current_dir(repo.git_dir())
.env("GIT_CONFIG_NOSYSTEM", "1")
.env("GIT_CONFIG_GLOBAL", directory.path().join("absent-config"))
.env("GIT_AUTHOR_NAME", "Maintenance Fixture")
.env("GIT_AUTHOR_EMAIL", "maintenance@example.com")
.env("GIT_COMMITTER_NAME", "Maintenance Fixture")
.env("GIT_COMMITTER_EMAIL", "maintenance@example.com")
.env("GIT_AUTHOR_DATE", "@1700000000 +0000")
.env("GIT_COMMITTER_DATE", "@1700000000 +0000")
.env("GIT_TRACE2_EVENT", &trace)
.args([
"-c",
"commit.gpgsign=false",
"maintenance",
"run",
"--task=commit-graph",
])
.output()
.unwrap();
assert!(output.status.success(), "{output:?}");
let events = std::fs::read_to_string(&trace).unwrap();
assert_eq!(
events.contains("\"label\":\"commit-graph\""),
!held,
"{events}"
);
std::fs::remove_file(&trace).unwrap();
if held {
drop(guard.take());
}
}
}
#[test]
fn existing_file_refuses_and_owned_file_is_removed() {
let directory = tempfile::tempdir().unwrap();
let repo = Repository::init(
crate::ObjectFormat::Sha1,
directory.path().join("r"),
crate::InitKind::Bare,
)
.unwrap();
let path = repo.object_dir().join("maintenance.lock");
std::fs::write(&path, b"another owner").unwrap();
assert!(matches!(repo.try_lock_maintenance(), Err(LockError::Busy)));
assert_eq!(std::fs::read(&path).unwrap(), b"another owner");
std::fs::remove_file(&path).unwrap();
let guard = repo.try_lock_maintenance().unwrap();
assert!(matches!(repo.try_lock_maintenance(), Err(LockError::Busy)));
drop(guard);
assert!(!path.exists());
repo.try_lock_maintenance().unwrap().release().unwrap();
assert!(!path.exists());
}
}