grit-lib 0.5.21

Core library for the grit Git implementation
//! Branch/tag ref names must match Git `check-ref-format` rules (#927).

use std::path::Path;
use std::process::Command;

use grit_lib::objects::ObjectId;
use grit_lib::refs::check_ref_format::{validate_branch_short_name, validate_tag_short_name};
use grit_lib::refs::write_ref;
use grit_lib::repo::{InitOptions, Repository};
use tempfile::tempdir;

fn git_check_ref_format_branch(name: &str) -> bool {
    Command::new("git")
        .args(["check-ref-format", "--branch", name])
        .env("GIT_CONFIG_GLOBAL", "/dev/null")
        .env("GIT_CONFIG_SYSTEM", "/dev/null")
        .status()
        .map(|s| s.success())
        .unwrap_or(false)
}

fn git_check_ref_format_tag(name: &str) -> bool {
    let full = format!("refs/tags/{name}");
    Command::new("git")
        .args(["check-ref-format", &full])
        .env("GIT_CONFIG_GLOBAL", "/dev/null")
        .env("GIT_CONFIG_SYSTEM", "/dev/null")
        .status()
        .map(|s| s.success())
        .unwrap_or(false)
}

fn git_fsck_strict(git_dir: &Path) -> bool {
    Command::new("git")
        .args([
            "-C",
            git_dir.to_str().expect("utf-8 path"),
            "fsck",
            "--strict",
        ])
        .env("GIT_CONFIG_GLOBAL", "/dev/null")
        .env("GIT_CONFIG_SYSTEM", "/dev/null")
        .status()
        .map(|s| s.success())
        .unwrap_or(false)
}

fn sample_oid() -> ObjectId {
    "67bf698f3ab735e92fb011a99cff3497c44d30c1".parse().unwrap()
}

fn seed_empty_commit(worktree: &Path) -> ObjectId {
    let status = Command::new("git")
        .current_dir(worktree)
        .args(["commit", "--allow-empty", "-m", "seed"])
        .env("GIT_CONFIG_GLOBAL", "/dev/null")
        .env("GIT_CONFIG_SYSTEM", "/dev/null")
        .env("GIT_AUTHOR_NAME", "Test")
        .env("GIT_AUTHOR_EMAIL", "t@example.com")
        .env("GIT_COMMITTER_NAME", "Test")
        .env("GIT_COMMITTER_EMAIL", "t@example.com")
        .status()
        .expect("git commit");
    assert!(status.success(), "git commit --allow-empty failed");
    let out = Command::new("git")
        .current_dir(worktree)
        .args(["rev-parse", "HEAD"])
        .output()
        .expect("rev-parse");
    assert!(out.status.success());
    String::from_utf8_lossy(&out.stdout)
        .trim()
        .parse()
        .expect("HEAD oid")
}

#[test]
fn branch_short_names_match_git_check_ref_format() {
    let samples = [
        "main",
        "feature/foo",
        "@",
        "-",
        "-topic",
        "bad name",
        "x..y",
        "a~b",
        "a.lock",
        "HEAD",
        "a:b",
        "a*b",
    ];
    for name in samples {
        let grit_ok = validate_branch_short_name(name).is_ok();
        let git_ok = git_check_ref_format_branch(name);
        assert_eq!(
            grit_ok, git_ok,
            "branch name {name:?}: grit={grit_ok} git={git_ok}"
        );
    }
}

#[test]
fn tag_short_names_match_git_check_ref_format() {
    let samples = [
        "v1",
        "release/1.0",
        "bad name",
        "a..b",
        "x.lock",
        "@",
        "HEAD",
    ];
    for name in samples {
        let grit_ok = validate_tag_short_name(name).is_ok();
        let git_ok = git_check_ref_format_tag(name);
        assert_eq!(
            grit_ok, git_ok,
            "tag name {name:?}: grit={grit_ok} git={git_ok}"
        );
    }
}

#[test]
fn write_ref_rejects_invalid_branch_names_without_loose_files() {
    let dir = tempdir().unwrap();
    let repo = Repository::init(
        dir.path(),
        &InitOptions::new()
            .with_initial_branch("main".to_owned())
            .with_ref_storage(grit_lib::RefStorageFormat::Files),
    )
    .expect("init");
    let git_dir = repo.git_dir().to_path_buf();
    let invalid = ["bad name", "x..y", "a~b", "a.lock"];
    for name in invalid {
        let refname = format!("refs/heads/{name}");
        assert!(
            write_ref(&git_dir, &refname, &sample_oid()).is_err(),
            "write_ref must reject {refname}"
        );
        let loose = git_dir.join("refs/heads").join(name);
        assert!(
            !loose.exists(),
            "must not leave loose ref at {}",
            loose.display()
        );
    }
    assert!(git_fsck_strict(dir.path()), "git fsck --strict must pass");
}

#[test]
fn write_ref_accepts_refs_heads_head_like_git() {
    let dir = tempdir().unwrap();
    let worktree = dir.path();
    let repo = Repository::init(
        worktree,
        &InitOptions::new()
            .with_initial_branch("main".to_owned())
            .with_ref_storage(grit_lib::RefStorageFormat::Files),
    )
    .expect("init");
    let tip = seed_empty_commit(worktree);
    write_ref(repo.git_dir(), "refs/heads/HEAD", &tip).expect("plumbing may write refs/heads/HEAD");
    assert!(
        git_fsck_strict(worktree),
        "git fsck --strict must pass on refs/heads/HEAD"
    );
}