use std::fs::{self, OpenOptions};
use std::io::{self, Write};
use std::path::PathBuf;
use std::sync::atomic::AtomicBool;
use crate::{ObjectFormat, ObjectId, ShallowRoots};
#[derive(Debug, thiserror::Error)]
pub enum FetchShallowError {
#[error("shallow metadata changed since fetch preparation")]
Changed,
#[error("shallow metadata")]
Read(#[from] crate::ShallowError),
#[error("shallow metadata I/O")]
Io(#[from] io::Error),
}
#[derive(Debug)]
pub(super) struct Lock {
path: PathBuf,
lock_path: PathBuf,
file: Option<crate::locks::SharedFile>,
#[cfg(unix)]
identity: fs::Metadata,
old: Vec<ObjectId>,
cleanup: crate::locks::Guard,
}
impl Lock {
pub(super) fn acquire(
common_dir: &std::path::Path,
format: ObjectFormat,
old: &[ObjectId],
cancel: &AtomicBool,
) -> Result<Self, FetchShallowError> {
let _lock_operation = crate::locks::operation();
let path = common_dir.join("shallow");
let lock_path = std::path::absolute(common_dir.join("shallow.lock"))?;
let mut options = OpenOptions::new();
options.write(true).create_new(true);
#[cfg(windows)]
{
use std::os::windows::fs::OpenOptionsExt;
options.share_mode(3); }
let file = options.open(&lock_path)?;
let identity = crate::locks::capture_identity(&lock_path, &file)?;
let file = crate::locks::SharedFile::from(file);
let cleanup = crate::locks::Guard::new(&lock_path, &file, identity.clone());
let lock = Self {
cleanup,
path,
lock_path,
file: Some(file),
#[cfg(unix)]
identity,
old: old.to_vec(),
};
let current = ShallowRoots::read(&lock.path, format, 16 * 1024 * 1024, cancel)?;
if current.iter().ne(old.iter().copied()) {
return Err(FetchShallowError::Changed);
}
Ok(lock)
}
pub(super) fn publish(&mut self, roots: &[ObjectId]) -> Result<bool, FetchShallowError> {
let _lock_operation = crate::locks::operation();
if self.old == roots {
return Ok(false);
}
self.check_owned()?;
let mut file = tempfile::NamedTempFile::new_in(self.path.parent().unwrap())?;
for id in roots {
writeln!(file, "{id}")?;
}
file.as_file().sync_all()?;
self.check_owned()?;
file.persist(&self.path).map_err(|error| error.error)?;
Ok(true)
}
fn check_owned(&self) -> Result<(), FetchShallowError> {
self.cleanup.check()?;
let current = fs::symlink_metadata(&self.lock_path)?;
if !current.is_file() || current.file_type().is_symlink() {
return Err(FetchShallowError::Changed);
}
#[cfg(unix)]
{
use std::os::unix::fs::MetadataExt;
if current.dev() != self.identity.dev() || current.ino() != self.identity.ino() {
return Err(FetchShallowError::Changed);
}
}
Ok(())
}
}
impl Drop for Lock {
fn drop(&mut self) {
let _lock_operation = crate::locks::operation();
if self.check_owned().is_ok() {
drop(self.file.take());
let _ = fs::remove_file(&self.lock_path);
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn changed_snapshot_and_foreign_lock_preserve_shallow_bytes() {
let root = tempfile::tempdir().unwrap();
let first = ObjectId::for_blob(ObjectFormat::Sha1, b"first");
let second = ObjectId::for_blob(ObjectFormat::Sha1, b"second");
let path = root.path().join("shallow");
fs::write(&path, format!("{first}\n")).unwrap();
assert!(matches!(
Lock::acquire(
root.path(),
ObjectFormat::Sha1,
&[second],
&AtomicBool::new(false)
),
Err(FetchShallowError::Changed)
));
assert!(!root.path().join("shallow.lock").exists());
fs::write(root.path().join("shallow.lock"), b"foreign").unwrap();
assert!(matches!(
Lock::acquire(
root.path(),
ObjectFormat::Sha1,
&[first],
&AtomicBool::new(false)
),
Err(FetchShallowError::Io(_))
));
assert_eq!(fs::read(&path).unwrap(), format!("{first}\n").as_bytes());
assert_eq!(
fs::read(root.path().join("shallow.lock")).unwrap(),
b"foreign"
);
}
#[cfg(unix)]
#[test]
fn cleanup_invalidates_shallow_publication_without_changing_roots() {
let root = tempfile::tempdir().unwrap();
let directory = root.path().canonicalize().unwrap();
let old = ObjectId::for_blob(ObjectFormat::Sha1, b"original root");
let new = ObjectId::for_blob(ObjectFormat::Sha1, b"new root");
let path = directory.join("shallow");
let bytes = format!("{old}\n");
fs::write(&path, &bytes).unwrap();
let mut lock = Lock::acquire(
&directory,
ObjectFormat::Sha1,
&[old],
&AtomicBool::new(false),
)
.unwrap();
assert_eq!(
crate::locks::cleanup_under(&directory)
.unwrap()
.removed
.len(),
1
);
assert!(lock.publish(&[new]).is_err());
assert_eq!(fs::read_to_string(path).unwrap(), bytes);
}
}