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

nordisk/edda

crates/gunnar-bitmap/tests/support/mod.rs

//! Fixture repositories built by stock `git`, so the bitmaps under test are the
//! ones `git` writes and not the ones this crate writes.
//!
//! Nothing here points at a path on the machine this was developed on. A test
//! that decodes only files a particular developer happens to have is a test that
//! silently stops running everywhere else, which is the same failure mode as a
//! guard that cannot go red.

// Each integration test binary compiles this module separately and uses a
// different subset of it, so unused-item warnings here are noise about the
// other binaries rather than about dead code.
#![allow(dead_code)]

use std::path::{Path, PathBuf};
use std::process::Command;

/// Runs `git` and fails loudly on a non-zero exit.
///
/// Checking the *output* rather than trusting the exit code matters: a
/// misspelled subcommand exits non-zero with an empty stdout, and an empty
/// stdout is shaped exactly like a legitimately empty result set.
pub fn git(repo: &Path, args: &[&str]) -> String {
    let out = Command::new("git")
        .arg("-C")
        .arg(repo)
        .args(args)
        .env("GIT_CONFIG_GLOBAL", "/dev/null")
        .env("GIT_CONFIG_SYSTEM", "/dev/null")
        .output()
        .unwrap_or_else(|e| panic!("spawn git {args:?}: {e}"));
    assert!(
        out.status.success(),
        "git {args:?} failed with {:?}\nstdout: {}\nstderr: {}",
        out.status.code(),
        String::from_utf8_lossy(&out.stdout),
        String::from_utf8_lossy(&out.stderr)
    );
    String::from_utf8(out.stdout).expect("git speaks utf-8 here")
}

/// Runs `git` and returns success plus the combined output, for the cases where
/// failure is the expected result.
pub fn git_try(repo: &Path, args: &[&str]) -> (bool, String) {
    let out = Command::new("git")
        .arg("-C")
        .arg(repo)
        .args(args)
        .env("GIT_CONFIG_GLOBAL", "/dev/null")
        .env("GIT_CONFIG_SYSTEM", "/dev/null")
        .output()
        .unwrap_or_else(|e| panic!("spawn git {args:?}: {e}"));
    let mut text = String::from_utf8_lossy(&out.stdout).into_owned();
    text.push_str(&String::from_utf8_lossy(&out.stderr));
    (out.status.success(), text)
}

/// A throwaway repository with a bitmap that stock git wrote.
pub struct Fixture {
    _dir: tempfile::TempDir,
    /// Path of the repository.
    pub repo: PathBuf,
    /// Which hash the repository uses.
    pub object_hash: gix_hash::Kind,
}

impl Fixture {
    /// Build a repository with enough history that the bitmap is interesting:
    /// branches that diverge and rejoin, a tag, files that change and files that
    /// do not, so the type bitmaps and the XOR chains are all exercised.
    pub fn build(commits: usize, object_hash: gix_hash::Kind) -> Self {
        let dir = tempfile::tempdir().expect("tempdir");
        let repo = dir.path().join("work");
        std::fs::create_dir_all(&repo).expect("mkdir");
        let format = match object_hash {
            gix_hash::Kind::Sha1 => "sha1",
            _ => "sha256",
        };
        assert!(Command::new("git")
            .args(["init", "-q", "--object-format", format, "-b", "main"])
            .arg(&repo)
            .env("GIT_CONFIG_GLOBAL", "/dev/null")
            .env("GIT_CONFIG_SYSTEM", "/dev/null")
            .status()
            .expect("git init")
            .success());
        git(&repo, &["config", "user.email", "t@example.com"]);
        git(&repo, &["config", "user.name", "T"]);

        std::fs::create_dir_all(repo.join("stable")).expect("mkdir");
        std::fs::write(repo.join("stable/never-changes.txt"), "constant\n").expect("write");

        for i in 0..commits {
            // A file that changes every commit, one directory that churns and
            // one that does not: this is what makes tree bitmaps differ from
            // blob bitmaps rather than moving together.
            std::fs::write(repo.join("churn.txt"), format!("rev {i}\n")).expect("write");
            if i % 3 == 0 {
                let d = repo.join(format!("dir{}", i % 7));
                std::fs::create_dir_all(&d).expect("mkdir");
                std::fs::write(d.join("f.txt"), format!("{i}\n")).expect("write");
            }
            git(&repo, &["add", "-A"]);
            git(&repo, &["commit", "-q", "-m", &format!("c{i}")]);
            if i == commits / 2 {
                git(&repo, &["branch", "side"]);
            }
        }
        // A side branch that diverges and merges back, so some commits are
        // reachable two ways and the bitmaps genuinely overlap.
        if commits > 4 {
            git(&repo, &["checkout", "-q", "side"]);
            std::fs::write(repo.join("side.txt"), "side\n").expect("write");
            git(&repo, &["add", "-A"]);
            git(&repo, &["commit", "-q", "-m", "side commit"]);
            git(&repo, &["checkout", "-q", "main"]);
            git(
                &repo,
                &["merge", "-q", "--no-ff", "-m", "merge side", "side"],
            );
        }
        git(&repo, &["tag", "-a", "-m", "a tag", "v1"]);

        // `-b` is what asks for the bitmap; `-a -d` makes it a single pack with
        // full closure, which the format requires (BITMAP_OPT_FULL_DAG).
        git(&repo, &["repack", "-adb", "-q"]);

        Fixture {
            _dir: dir,
            repo,
            object_hash,
        }
    }

    /// Path of the single `.bitmap` in the repository.
    pub fn bitmap_path(&self) -> PathBuf {
        self.pack_file("bitmap")
    }

    /// Path of the pack `.idx`.
    pub fn idx_path(&self) -> PathBuf {
        self.pack_file("idx")
    }

    fn pack_file(&self, ext: &str) -> PathBuf {
        let dir = self.repo.join(".git/objects/pack");
        let mut found: Vec<PathBuf> = std::fs::read_dir(&dir)
            .unwrap_or_else(|e| panic!("read {dir:?}: {e}"))
            .filter_map(|e| e.ok().map(|e| e.path()))
            .filter(|p| p.extension().is_some_and(|x| x == ext))
            .collect();
        assert_eq!(
            found.len(),
            1,
            "expected exactly one .{ext} in {dir:?}, found {found:?}"
        );
        found.pop().expect("checked")
    }

    /// The raw bytes of the bitmap git wrote.
    pub fn bitmap_bytes(&self) -> Vec<u8> {
        std::fs::read(self.bitmap_path()).expect("read bitmap")
    }

    /// Map pack positions to object ids through the real `.idx`.
    pub fn index(&self) -> gix_pack::index::File {
        gix_pack::index::File::at(self.idx_path(), self.object_hash).expect("open idx")
    }

    /// The pack-position <-> index-position permutation for this pack.
    ///
    /// Bitmap bits are pack positions, so nothing can be looked up without this.
    pub fn pack_order(&self, idx: &gix_pack::index::File) -> gunnar_bitmap::PackOrder {
        let mut offsets = vec![0u64; idx.num_objects() as usize];
        for entry in idx.iter() {
            offsets[idx
                .lookup(entry.oid.as_ref())
                .expect("oid is in its own index") as usize] = entry.pack_offset;
        }
        gunnar_bitmap::PackOrder::from_index_offsets(&offsets)
    }
}

/// Object ids `git rev-list --objects` reports, as a sorted set of hex strings.
///
/// The second column of `--objects` output is a path, which is why only the
/// first token is taken.
pub fn rev_list_objects(repo: &Path, args: &[&str]) -> Vec<String> {
    let mut all = vec!["rev-list", "--objects"];
    all.extend_from_slice(args);
    let out = git(repo, &all);
    let mut ids: Vec<String> = out
        .lines()
        .filter_map(|l| l.split_whitespace().next())
        .map(|s| s.to_owned())
        .collect();
    ids.sort();
    ids.dedup();
    ids
}