use std::fs::{self, File};
use std::io::{self, Read};
use std::path::Path;
use std::sync::atomic::{AtomicBool, Ordering};
use tempfile::NamedTempFile;
use super::RetentionPolicy;
use crate::pack::{PackObject, PackWriteError, PackWriteLimits, PackWritten, write_controlled};
use crate::{ObjectId, ObjectReadError, PackCompression, Repository};
#[derive(Debug, Clone, Copy)]
pub struct RepackLimits {
pub write: PackWriteLimits,
pub compression: PackCompression,
}
impl Default for RepackLimits {
fn default() -> Self {
Self {
write: PackWriteLimits::default(),
compression: PackCompression::Ordinary,
}
}
}
#[derive(Debug, Clone, Copy)]
pub struct RepackPublished {
pub written: PackWritten,
}
#[derive(Debug, thiserror::Error)]
pub enum RepackError {
#[error("retention scan incomplete: {0}")]
Incomplete(String),
#[error("required object missing: {0}")]
Missing(ObjectId),
#[error("cannot read required object {id}")]
Read {
id: ObjectId,
#[source]
source: ObjectReadError,
},
#[error(transparent)]
Open(#[from] ObjectReadError),
#[error(transparent)]
Write(#[from] PackWriteError),
#[error("repack cancelled")]
Cancelled,
#[error("conflicting existing pack artifact: {0}")]
Existing(std::path::PathBuf),
#[error(transparent)]
Io(#[from] io::Error),
}
impl Repository {
pub fn repack_retained(
&self,
policy: &RetentionPolicy,
limits: RepackLimits,
cancel: &AtomicBool,
) -> Result<RepackPublished, RepackError> {
self.repack_retained_after_plan(policy, limits, cancel, || {})
}
fn repack_retained_after_plan(
&self,
policy: &RetentionPolicy,
limits: RepackLimits,
cancel: &AtomicBool,
after_plan: impl FnOnce(),
) -> Result<RepackPublished, RepackError> {
check(cancel)?;
let plan = self.plan_retention(policy, cancel);
if let super::RetentionOutcome::Incomplete(reason) = plan.outcome {
return Err(RepackError::Incomplete(reason));
}
after_plan();
check(cancel)?;
let objects =
self.objects_controlled(policy.packs, crate::AlternateLimits::default(), cancel)?;
let count = u32::try_from(plan.required.len())
.map_err(|_| PackWriteError::Limit("object count"))?;
if count > limits.write.max_objects {
return Err(PackWriteError::Limit("object count").into());
}
let mut decoded = Vec::with_capacity(plan.required.len());
let mut bytes = 0u64;
for id in plan.required {
check(cancel)?;
let object = objects
.read_controlled(id, policy.read, cancel)
.map_err(|source| RepackError::Read { id, source })?
.or_else(|| super::canonical_empty_tree(id))
.ok_or(RepackError::Missing(id))?;
let len = object.data().len() as u64;
if len > limits.write.max_object_bytes {
return Err(PackWriteError::Limit("object bytes").into());
}
bytes = bytes
.checked_add(len)
.ok_or(PackWriteError::Limit("input bytes"))?;
if bytes > limits.write.max_input_bytes {
return Err(PackWriteError::Limit("input bytes").into());
}
decoded.push((id, object));
}
let input: Vec<_> = decoded
.iter()
.map(|(id, object)| PackObject {
id: *id,
kind: object.kind(),
data: object.data(),
})
.collect();
let directory = self.object_dir().join("pack");
fs::create_dir_all(&directory)?;
let mut pack = NamedTempFile::new_in(&directory)?;
let mut index = NamedTempFile::new_in(&directory)?;
let write_result = write_controlled(
self.object_format(),
&input,
&mut pack,
&mut index,
limits.write,
limits.compression,
&mut || {
if cancel.load(Ordering::Relaxed) {
Err(PackWriteError::Io(io::ErrorKind::Interrupted.into()))
} else {
Ok(())
}
},
);
let written = match write_result {
Err(_) if cancel.load(Ordering::Relaxed) => return Err(RepackError::Cancelled),
result => result?,
};
pack.as_file().sync_all()?;
index.as_file().sync_all()?;
let basename = directory.join(format!("pack-{}", written.checksum));
publish_pair(pack, index, &basename, cancel, || {})?;
Ok(RepackPublished { written })
}
}
fn publish_pair(
pack: NamedTempFile,
index: NamedTempFile,
basename: &Path,
cancel: &AtomicBool,
after_pack: impl FnOnce(),
) -> Result<(), RepackError> {
check(cancel)?;
publish(pack, &basename.with_extension("pack"), cancel)?;
after_pack();
check(cancel)?;
publish(index, &basename.with_extension("idx"), cancel)?;
Ok(())
}
fn check(cancel: &AtomicBool) -> Result<(), RepackError> {
if cancel.load(Ordering::Relaxed) {
Err(RepackError::Cancelled)
} else {
Ok(())
}
}
fn publish(temporary: NamedTempFile, path: &Path, cancel: &AtomicBool) -> Result<(), RepackError> {
match temporary.persist_noclobber(path) {
Ok(_) => Ok(()),
Err(error) if error.error.kind() == io::ErrorKind::AlreadyExists => {
let mut existing = File::open(path)?;
let mut candidate = error.file.reopen()?;
if existing.metadata()?.len() != candidate.metadata()?.len() {
return Err(RepackError::Existing(path.into()));
}
let mut a = [0; 8192];
let mut b = [0; 8192];
loop {
check(cancel)?;
let n = existing.read(&mut a)?;
candidate.read_exact(&mut b[..n])?;
if a[..n] != b[..n] {
return Err(RepackError::Existing(path.into()));
}
if n == 0 {
return Ok(());
}
}
}
Err(error) => Err(error.error.into()),
}
}
#[cfg(test)]
mod tests {
use std::io::Write;
use super::*;
#[test]
fn cancellation_between_publications_leaves_only_an_unindexed_pack() {
let root = tempfile::tempdir().unwrap();
let basename = root.path().join("pack-test");
let mut pack = NamedTempFile::new_in(root.path()).unwrap();
let mut index = NamedTempFile::new_in(root.path()).unwrap();
pack.write_all(b"pack bytes").unwrap();
index.write_all(b"index bytes").unwrap();
let cancelled = AtomicBool::new(false);
let result = publish_pair(pack, index, &basename, &cancelled, || {
assert!(basename.with_extension("pack").exists());
assert!(!basename.with_extension("idx").exists());
cancelled.store(true, Ordering::Relaxed);
});
assert!(matches!(result, Err(RepackError::Cancelled)));
assert_eq!(
fs::read(basename.with_extension("pack")).unwrap(),
b"pack bytes"
);
assert!(!basename.with_extension("idx").exists());
}
#[test]
fn conflicting_existing_artifact_is_not_overwritten() {
let root = tempfile::tempdir().unwrap();
let path = root.path().join("pack-existing.pack");
fs::write(&path, b"old bytes").unwrap();
let mut candidate = NamedTempFile::new_in(root.path()).unwrap();
candidate.write_all(b"new bytes").unwrap();
let result = publish(candidate, &path, &AtomicBool::new(false));
assert!(matches!(result, Err(RepackError::Existing(_))));
assert_eq!(fs::read(path).unwrap(), b"old bytes");
}
#[test]
fn new_loose_object_after_scan_survives_additive_publication() {
let root = tempfile::tempdir().unwrap();
let repository = Repository::init(
crate::ObjectFormat::Sha1,
root.path().join("repo"),
crate::InitKind::Bare,
)
.unwrap();
let mut new_id = None;
repository
.repack_retained_after_plan(
&RetentionPolicy::default(),
RepackLimits::default(),
&AtomicBool::new(false),
|| {
new_id = Some(
repository
.loose_objects()
.write_blob(b"new writer")
.unwrap(),
)
},
)
.unwrap();
let id = new_id.unwrap();
assert_eq!(
repository.loose_objects().read_blob(id, 64).unwrap(),
b"new writer"
);
}
}