monochromatic-git-policy-cli 0.5.2

Repository-owned Git policy wrapper: forwards Git commands and checks commits against configured policies.
Documentation
//! Controls for finding the pointers a push introduces, against repositories built by real Git.

use super::*;
use crate::repo_read_object::from_hex;
use crate::test_support::{fixture, git, git_output, remove};
use std::path::{Path, PathBuf};

/// The object name that the `n`-th fixture pointer names: 64 hexadecimal digits.
fn oid(n: u8) -> String {
    return format!("{n:02x}").repeat(32);
}

/// The text of a pointer file for the `n`-th fixture object.
fn pointer(n: u8) -> String {
    return format!("version https://git-lfs.github.com/spec/v1\noid sha256:{}\nsize {}\n", oid(n), 100 + u32::from(n));
}

/// Write `files` (path, text) into the repository and commit them; the commit's name.
fn commit(repo: &Path, files: &[(&str, &str)], message: &str) -> String {
    for (path, text) in files {
        let target: PathBuf = repo.join(path);
        std::fs::create_dir_all(target.parent().expect("a parent")).expect("directories");
        std::fs::write(target, text).expect("file");
    }
    git(repo, &["add", "--all"]);
    git(repo, &["commit", "--quiet", "--allow-empty", &format!("--message={message}")]);
    return name_of(repo, "HEAD");
}

/// The object name `revision` stands for.
fn name_of(repo: &Path, revision: &str) -> String {
    return String::from_utf8(git_output(repo, &["rev-parse", revision]).stdout).expect("UTF-8").trim().to_owned();
}

/// A repository with the commits `base` (a pointer 1 and plain text), `added` (pointer 2 beside it), `removed` (pointer 2
///  gone again, a plain edit) and a plain `tip`; their names in that order.
fn history(root: &Path) -> (PathBuf, [String; 4]) {
    let repo: PathBuf = root.join("repo");
    std::fs::create_dir(&repo).expect("dir");
    git(repo.as_path(), &["init", "--quiet", "--initial-branch=main"]);
    let base: String = commit(repo.as_path(), &[("docs/readme.txt", "one\n"), ("assets/one.bin", pointer(1).as_str())], "base");
    let added: String = commit(repo.as_path(), &[("assets/two.bin", pointer(2).as_str())], "added");
    std::fs::remove_file(repo.join("assets/two.bin")).expect("remove");
    let removed: String = commit(repo.as_path(), &[("docs/readme.txt", "two\n")], "removed");
    let tip: String = commit(repo.as_path(), &[("docs/other.txt", "three\n")], "tip");
    return (repo, [base, added, removed, tip]);
}

/// The scan from `include` tips and `stop` tips, in the repository's own format.
fn scan(repo: &Path, include: &[&str], stop: &[&str]) -> Option<Vec<String>> {
    let format = ObjectFormat::Sha1;
    let database = ObjectDatabase::open(repo.join(".git").as_path(), None, format).expect("plain database");
    let names = |tips: &[&str]| -> Vec<Vec<u8>> {
        return tips.iter().map(|tip| { return from_hex(tip.as_bytes(), format).expect("name"); }).collect();
    };
    return new_pointer_objects(&database, format, names(include).as_slice(), names(stop).as_slice(), SCAN_BUDGET);
}

/// Commits that touch no pointer introduce none, whatever pointers the history behind them holds.
#[test]
fn commits_without_a_pointer_introduce_none() {
    let root = fixture("lfs-scan-none");
    let (repo, [base, _added, removed, tip]) = history(root.as_path());
    assert_eq!(scan(repo.as_path(), &[tip.as_str()], &[removed.as_str()]), Some(Vec::new()));
    assert_eq!(scan(repo.as_path(), &[tip.as_str()], &[tip.as_str()]), Some(Vec::new()), "nothing is ahead of itself");
    assert_eq!(scan(repo.as_path(), &[removed.as_str()], &[base.as_str()]).map(|found| { return found.len(); }), Some(1), "the control: the range holding the intermediate commit finds one");
    remove(root.as_path());
}

/// A push that adds one pointer names exactly that object, and not the ones the destination already has.
#[test]
fn a_new_pointer_is_named_and_a_known_one_is_not() {
    let root = fixture("lfs-scan-one");
    let (repo, [base, added, _removed, _tip]) = history(root.as_path());
    assert_eq!(scan(repo.as_path(), &[added.as_str()], &[base.as_str()]), Some(vec![oid(2)]));
    remove(root.as_path());
}

/// A pointer that one commit adds and the next removes is still uploaded: the destination will receive both commits.
#[test]
fn a_pointer_added_and_removed_inside_the_range_is_still_named() {
    let root = fixture("lfs-scan-transient");
    let (repo, [base, _added, _removed, tip]) = history(root.as_path());
    assert_eq!(scan(repo.as_path(), &[tip.as_str()], &[base.as_str()]), Some(vec![oid(2)]));
    remove(root.as_path());
}

/// With nothing known to the destination the whole history is read and every pointer in it is named, once.
#[test]
fn without_known_tips_the_whole_history_is_scanned() {
    let root = fixture("lfs-scan-all");
    let (repo, [_base, _added, _removed, tip]) = history(root.as_path());
    assert_eq!(scan(repo.as_path(), &[tip.as_str()], &[]), Some(vec![oid(1), oid(2)]));
    remove(root.as_path());
}

/// A merge of an old side branch names only the side branch's own pointer: the history behind the known tip is known.
#[test]
fn a_merge_names_what_neither_parent_holds_and_each_object_once() {
    let root = fixture("lfs-scan-merge");
    let (repo, [base, _added, _removed, tip]) = history(root.as_path());
    git(repo.as_path(), &["checkout", "--quiet", "-b", "side", base.as_str()]);
    let side: String = commit(repo.as_path(), &[("assets/three.bin", pointer(3).as_str())], "side");
    git(repo.as_path(), &["checkout", "--quiet", "main"]);
    git(repo.as_path(), &["merge", "--quiet", "--no-ff", "--message=merge", "side"]);
    let merge: String = name_of(repo.as_path(), "HEAD");
    assert_eq!(scan(repo.as_path(), &[merge.as_str()], &[tip.as_str()]), Some(vec![oid(3)]), "pointer 1 sits behind the known tip and is not dragged in");
    assert_eq!(scan(repo.as_path(), &[merge.as_str()], &[tip.as_str(), side.as_str()]), Some(Vec::new()), "both parents known: the merge adds nothing");
    remove(root.as_path());
}

/// Blobs that are not pointers (large, or lookalikes) are never named, whatever their path.
#[test]
fn lookalikes_and_large_files_are_not_named() {
    let root = fixture("lfs-scan-lookalike");
    let (repo, [_base, _added, _removed, tip]) = history(root.as_path());
    let lookalike: String = pointer(9).replace("size", "length");
    let large: String = format!("{}{}", pointer(8), "x".repeat(2000));
    let top: String = commit(repo.as_path(), &[("notes/mention.txt", "see git-lfs docs\n"), ("lookalike.bin", lookalike.as_str()), ("large.bin", large.as_str())], "lookalikes");
    assert_eq!(scan(repo.as_path(), &[top.as_str()], &[tip.as_str()]), Some(Vec::new()));
    remove(root.as_path());
}

/// Whatever the readers cannot answer declines instead of guessing: an unknown tip, a stop that is not a commit's parent
///  chain in the store, a spent budget and a pointer with an extension line.
#[test]
fn what_the_readers_cannot_answer_declines() {
    let root = fixture("lfs-scan-decline");
    let (repo, [_base, _added, _removed, tip]) = history(root.as_path());
    let missing: String = "ab".repeat(20);
    assert_eq!(scan(repo.as_path(), &[missing.as_str()], &[]), None, "an unknown tip");
    let format = ObjectFormat::Sha1;
    let database = ObjectDatabase::open(repo.join(".git").as_path(), None, format).expect("database");
    let tip_name: Vec<Vec<u8>> = vec![from_hex(tip.as_bytes(), format).expect("name")];
    assert_eq!(new_pointer_objects(&database, format, tip_name.as_slice(), &[], Duration::ZERO), None, "a spent budget");
    let extended: String = format!("version https://git-lfs.github.com/spec/v1\next-0-foo sha256:{}\noid sha256:{}\nsize 9\n", oid(4), oid(5));
    let top: String = commit(repo.as_path(), &[("ext.bin", extended.as_str())], "extension");
    assert_eq!(scan(repo.as_path(), &[top.as_str()], &[tip.as_str()]), None, "an extension line");
    remove(root.as_path());
}