Your browser sucks, JavaScript disabled! Gunnar's console needs JavaScript and WebAssembly. Git over SSH on port 2222 does not.

nordisk/edda

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()
}