crates/storage-git-gix/parked-gix-2/coldtier-archive/unfold.rs
//! The read half: a repository reconstituted out of its archive.
//!
//! [`unfold`] is what makes the cold tier a *tier* rather than a backup. It
//! writes the pack and its index back into `objects/pack`, in the order git's
//! own atomicity depends on — data first, index last, because `gix-odb` and
//! `git` both discover a pack *through its `.idx`*, so the pack becomes visible
//! at the instant the index lands and never before.
//!
//! # What "byte-identical" means here and why it is asserted
//!
//! A pack's whole value is that its entries are still deflated and still
//! deltified. A read-back that returned equivalent objects rather than the same
//! bytes would have quietly lost that, and every functional test would still
//! pass — the clone works, the objects are right, and `P-001`'s 18.3× on wire
//! bytes is gone. So [`UnfoldReport`] carries the two files' lengths and the
//! caller compares bytes, and the e2e test re-derives the pack id from what came
//! out of the archive.
use std::collections::BTreeMap;
use std::path::{Path, PathBuf};
use anyhow::{anyhow, bail, Context as _, Result};
use znippy_common::{get_file, ZnippyArchive, ZnippyReader};
use znippy_plugin_git::{pack_path_kind, read_refs, PackFileKind, RefState};
/// What one unfold wrote.
#[derive(Debug, Clone, PartialEq, Eq, Default)]
pub struct UnfoldReport {
/// The `pack-<id>.pack` that was written, and its byte length.
pub pack: Option<(PathBuf, u64)>,
/// Its `pack-<id>.idx`.
pub index: Option<(PathBuf, u64)>,
/// Refs recovered from `__gunnar_refs__`. Empty when the archive carries no
/// such section, which [`refs_present`](Self::refs_present) distinguishes
/// from a repository that has none.
pub refs: BTreeMap<String, RefState>,
/// Whether the archive carried a `__gunnar_refs__` section at all.
pub refs_present: bool,
}
/// Read one entry's bytes out of an archive.
pub fn read_entry(archive: &Path, name: &str) -> Result<Vec<u8>> {
get_file(archive, name).map_err(|e| anyhow!("reading '{name}' from {}: {e}", archive.display()))
}
/// Write the archive's pack tier into `git_dir`, and recover its refs.
///
/// `git_dir` need not exist; `objects/pack` is created. Nothing else in the
/// repository is touched — this writes the object tier and hands the refs back
/// for the caller to apply through its own [`RefStore`](gunnar_store::RefStore),
/// rather than writing ref files behind the store's back.
pub fn unfold(archive: &Path, git_dir: &Path) -> Result<UnfoldReport> {
let entries = ZnippyArchive::open(archive)
.map_err(|e| anyhow!("opening {}: {e}", archive.display()))?
.list_files()
.map_err(|e| anyhow!("listing {}: {e}", archive.display()))?;
let pack_dir = git_dir.join("objects").join("pack");
std::fs::create_dir_all(&pack_dir)?;
// Sorted so the `.idx` is written after the `.pack` regardless of the order
// the archive lists them in: `.idx` < `.pack` lexicographically, so the sort
// is reversed. Stated because getting it backwards produces a repository
// that is briefly discoverable and briefly incomplete, which is a race a
// test is unlikely to catch and a concurrent reader certainly will.
let mut pack_entries: Vec<(&String, PackFileKind)> = entries
.iter()
.filter_map(|name| pack_path_kind(name).map(|k| (name, k)))
.collect();
pack_entries.sort_by_key(|(_, kind)| match kind {
PackFileKind::Data => 0u8,
PackFileKind::Index => 1,
});
let mut report = UnfoldReport::default();
for (name, kind) in pack_entries {
let bytes = read_entry(archive, name)?;
let dst = pack_dir.join(name);
write_durable(&dst, &bytes).with_context(|| format!("writing {}", dst.display()))?;
let slot = match kind {
PackFileKind::Data => &mut report.pack,
PackFileKind::Index => &mut report.index,
};
if let Some((existing, _)) = slot {
bail!(
"the archive holds two {kind:?} files, {} and {name}; a repository's cold tier is \
one consolidated pack",
existing.display()
);
}
*slot = Some((dst, bytes.len() as u64));
}
if report.pack.is_some() != report.index.is_some() {
bail!(
"the archive holds a pack without its index, or the reverse: {:?} / {:?}. A pack with \
no index is invisible to every reader; an index with no pack is a reader promised \
objects that are not there.",
report.pack.as_ref().map(|(p, _)| p.display().to_string()),
report.index.as_ref().map(|(p, _)| p.display().to_string()),
);
}
// The directory entries have to be durable, or a power cut after this
// returns leaves a repository whose pack is listed and empty.
if let Ok(f) = std::fs::File::open(&pack_dir) {
let _ = f.sync_all();
}
match read_refs(archive).map_err(|e| anyhow!("reading __gunnar_refs__: {e}"))? {
Some(refs) => {
report.refs_present = true;
report.refs = refs;
}
None => report.refs_present = false,
}
Ok(report)
}
fn write_durable(path: &Path, bytes: &[u8]) -> std::io::Result<()> {
use std::io::Write as _;
let mut f = std::fs::File::create(path)?;
f.write_all(bytes)?;
f.sync_all()
}