grit-lib 0.5.21

Core library for the grit Git implementation
//! Index batch removal and replacement round-trip with system `git ls-files` / `fsck`.

use std::process::Command;

use grit_lib::index::{Index, IndexEntry, MODE_REGULAR};
use grit_lib::objects::{ObjectId, ObjectKind};
use grit_lib::repo::{InitOptions, Repository};
use grit_test_support::git;

fn ls_files_stage_lines(repo_dir: &std::path::Path) -> Vec<String> {
    let out = Command::new("git")
        .current_dir(repo_dir)
        .args(["ls-files", "-s"])
        .env("GIT_CONFIG_GLOBAL", "/dev/null")
        .env("GIT_CONFIG_SYSTEM", "/dev/null")
        .output()
        .expect("git ls-files -s");
    assert!(
        out.status.success(),
        "git ls-files -s failed: {}",
        String::from_utf8_lossy(&out.stderr)
    );
    String::from_utf8_lossy(&out.stdout)
        .lines()
        .map(str::to_owned)
        .collect()
}

fn expected_ls_files_lines(entries: &[IndexEntry]) -> Vec<String> {
    let mut sorted: Vec<&IndexEntry> = entries.iter().collect();
    sorted.sort_by(|a, b| a.path.cmp(&b.path).then_with(|| a.stage().cmp(&b.stage())));
    sorted
        .into_iter()
        .map(|e| {
            format!(
                "{:o} {} {}\t{}",
                e.mode,
                e.oid.to_hex(),
                e.stage(),
                String::from_utf8_lossy(&e.path)
            )
        })
        .collect()
}

fn fsck_clean(repo_dir: &std::path::Path) {
    let out = Command::new("git")
        .current_dir(repo_dir)
        .args(["fsck", "--strict"])
        .env("GIT_CONFIG_GLOBAL", "/dev/null")
        .env("GIT_CONFIG_SYSTEM", "/dev/null")
        .output()
        .expect("git fsck");
    assert!(
        out.status.success(),
        "git fsck failed: {}",
        String::from_utf8_lossy(&out.stderr)
    );
}

fn entry_for_path(repo: &Repository, path: &str, stage: u8, payload: &[u8]) -> IndexEntry {
    let oid = repo
        .odb()
        .write(ObjectKind::Blob, payload)
        .expect("write blob");
    let mut e = IndexEntry {
        ctime_sec: 1,
        ctime_nsec: 0,
        mtime_sec: 1,
        mtime_nsec: 0,
        dev: 0,
        ino: 0,
        mode: MODE_REGULAR,
        uid: 0,
        gid: 0,
        size: payload.len() as u32,
        oid,
        flags: path.len().min(0xFFF) as u16,
        flags_extended: None,
        path: path.as_bytes().to_vec(),
        base_index_pos: 0,
    };
    e.flags = (e.flags & 0x0FFF) | ((stage as u16 & 0x3) << 12);
    e
}

#[test]
fn grit_index_batch_remove_replace_matches_git_ls_files_and_fsck() {
    let tmp = tempfile::tempdir().expect("tempdir");
    let repo = Repository::init(
        tmp.path(),
        &InitOptions::new()
            .with_initial_branch("main".to_owned())
            .with_ref_storage(grit_lib::RefStorageFormat::Files),
    )
    .expect("init");
    let payload = b"index-compat-payload\n";

    let mut index = Index::new();
    const TOTAL: usize = 5_000;
    for i in 0..TOTAL {
        let path = format!("f/{i:04}.txt");
        if i % 37 == 0 {
            for stage in 1u8..=3 {
                index.add_or_replace(entry_for_path(&repo, &path, stage, payload));
            }
        } else {
            index.add_or_replace(entry_for_path(&repo, &path, 0, payload));
        }
    }

    let touch_count = TOTAL / 10;
    let mut remove_paths: Vec<Vec<u8>> = index
        .entries()
        .iter()
        .filter(|e| e.stage() == 0)
        .take(touch_count)
        .map(|e| e.path.clone())
        .collect();
    remove_paths.sort();
    remove_paths.dedup();

    let replacement_oid: ObjectId = repo
        .odb()
        .write(ObjectKind::Blob, b"replacement\n")
        .expect("replacement blob");
    let replacements: Vec<IndexEntry> = remove_paths
        .iter()
        .map(|path| {
            let mut e = entry_for_path(
                &repo,
                std::str::from_utf8(path).unwrap(),
                0,
                b"replacement\n",
            );
            e.oid = replacement_oid;
            e
        })
        .collect();
    index.remove_paths_and_insert(remove_paths.iter().map(|p| p.as_slice()), replacements);

    repo.index_handle().write(&mut index).expect("write index");

    let git_lines = ls_files_stage_lines(tmp.path());
    let expected = expected_ls_files_lines(index.entries());
    assert_eq!(git_lines, expected);

    git(tmp.path(), &["add", "-A"]);
    fsck_clean(tmp.path());
}