grit-lib 0.5.21

Core library for the grit Git implementation
//! Init initial branch, separate git dir, and object format (task 3527 / t0001-init core cases).

use std::fs;
use std::process::Command;

use grit_lib::config::environment::Environment;
use grit_lib::error::Error;
use grit_lib::objects::HashAlgo;
use grit_lib::progress::NullProgress;
use grit_lib::repo::{resolve_initial_branch, InitOptions, Repository};
use tempfile::TempDir;

fn git_fsck_ok(git_dir: &std::path::Path) {
    let output = Command::new("git")
        .args(["--git-dir", &git_dir.to_string_lossy(), "fsck"])
        .output()
        .expect("spawn git fsck");
    assert!(
        output.status.success(),
        "git fsck failed: {}",
        String::from_utf8_lossy(&output.stderr)
    );
}

fn write_global_config(dir: &TempDir, contents: &str) {
    let path = dir.path().join("gitconfig");
    fs::write(&path, contents).expect("write gitconfig");
}

fn env_with_global(global_config: &std::path::Path) -> Environment {
    let mut env = Environment::empty();
    env.git_config_global = Some(global_config.to_string_lossy().into_owned());
    env
}

#[test]
fn init_uses_explicit_initial_branch() {
    let td = TempDir::new().expect("tempdir");
    let repo = Repository::init(
        td.path(),
        &InitOptions::new().with_initial_branch("develop"),
    )
    .expect("init");
    let head = fs::read_to_string(repo.git_dir().join("HEAD")).expect("read HEAD");
    assert_eq!(head.trim(), "ref: refs/heads/develop");
}

#[test]
fn init_honors_init_default_branch_from_global_config() {
    let cfg_home = TempDir::new().expect("tempdir");
    write_global_config(&cfg_home, "[init]\n\tdefaultBranch = production\n");
    let env = env_with_global(&cfg_home.path().join("gitconfig"));
    assert_eq!(
        resolve_initial_branch(&env, None).expect("resolve"),
        "production"
    );
    let wt = TempDir::new().expect("tempdir");
    let repo =
        Repository::init(wt.path(), &InitOptions::new().with_environment(env)).expect("init");
    let head = fs::read_to_string(repo.git_dir().join("HEAD")).expect("read HEAD");
    assert_eq!(head.trim(), "ref: refs/heads/production");
}

#[test]
fn init_falls_back_to_main() {
    let env = Environment::empty();
    assert_eq!(resolve_initial_branch(&env, None).expect("resolve"), "main");
}

#[test]
fn init_invalid_branch_name_is_typed_error() {
    let err = resolve_initial_branch(&Environment::empty(), Some("-bad")).unwrap_err();
    assert!(matches!(err, Error::InvalidRef(_)));
}

#[test]
fn reinit_keeps_existing_head_branch() {
    let td = TempDir::new().expect("tempdir");
    Repository::init(td.path(), &InitOptions::new().with_initial_branch("first")).expect("init");
    let (_, outcome) = Repository::init_with_outcome(
        td.path(),
        &InitOptions::new().with_initial_branch("ignored"),
    )
    .expect("reinit");
    assert!(outcome.reinitialized);
    assert_eq!(outcome.initial_branch, "first");
    let head = fs::read_to_string(td.path().join(".git/HEAD")).expect("read HEAD");
    assert_eq!(head.trim(), "ref: refs/heads/first");
}

#[test]
fn init_separate_git_dir_git_status_works() {
    let wt = TempDir::new().expect("tempdir");
    let separate = TempDir::new().expect("tempdir");
    let repo = Repository::init(
        wt.path(),
        &InitOptions::new()
            .with_initial_branch("main")
            .with_separate_git_dir(Some(separate.path().to_path_buf())),
    )
    .expect("init");
    git_fsck_ok(repo.git_dir());
    let status = Command::new("git")
        .current_dir(wt.path())
        .args(["status"])
        .output()
        .expect("git status");
    assert!(
        status.status.success(),
        "git status failed: {}",
        String::from_utf8_lossy(&status.stderr)
    );
    let gitfile = fs::read_to_string(wt.path().join(".git")).expect("gitfile");
    assert!(gitfile.starts_with("gitdir:"));
}

#[test]
fn init_sha256_then_commit_git_fsck_clean() {
    let td = TempDir::new().expect("tempdir");
    let repo = Repository::init(
        td.path(),
        &InitOptions::new()
            .with_initial_branch("main")
            .with_object_format(HashAlgo::Sha256),
    )
    .expect("init");
    assert_eq!(repo.odb().hash_algo(), HashAlgo::Sha256);
    let format = Command::new("git")
        .current_dir(td.path())
        .args(["rev-parse", "--show-object-format"])
        .output()
        .expect("rev-parse");
    assert!(
        format.status.success(),
        "{}",
        String::from_utf8_lossy(&format.stderr)
    );
    assert_eq!(String::from_utf8_lossy(&format.stdout).trim(), "sha256");

    fs::write(td.path().join("file.txt"), "hello\n").expect("write");
    grit_lib::porcelain::add::stage(
        &repo,
        &grit_lib::porcelain::add::StageOptions::default(),
        &mut NullProgress,
    )
    .expect("stage");
    let req = grit_lib::porcelain::commit::CommitRequest {
        message: "test".into(),
        author: Some("T <t@e.com> 1 +0000".parse().expect("signature")),
        committer: Some("T <t@e.com> 1 +0000".parse().expect("signature")),
        allow_empty: false,
        sign_override: None,
        amend: false,
        assume_worktree_synced: false,
        skip_pre_commit_tree_match: false,
        staged_change_count: None,
        ..Default::default()
    };
    grit_lib::porcelain::commit::create_commit(&repo, &req, &mut NullProgress).expect("commit");
    git_fsck_ok(repo.git_dir());
}

#[test]
fn init_reinit_preserves_sha256_when_object_format_omitted() {
    let td = TempDir::new().expect("tempdir");
    Repository::init(
        td.path(),
        &InitOptions::new()
            .with_initial_branch("main")
            .with_object_format(HashAlgo::Sha256),
    )
    .expect("init sha256");
    let (_, outcome) = Repository::init_with_outcome(td.path(), &InitOptions::new())
        .expect("reinit without object format");
    assert!(outcome.reinitialized);
    let repo = Repository::init(td.path(), &InitOptions::new()).expect("open");
    assert_eq!(repo.odb().hash_algo(), HashAlgo::Sha256);
}

#[test]
fn init_rejects_bare_with_separate_git_dir() {
    let wt = TempDir::new().expect("tempdir");
    let separate = TempDir::new().expect("tempdir");
    let err = Repository::init_with_outcome(
        wt.path(),
        &InitOptions::new()
            .with_bare(true)
            .with_separate_git_dir(Some(separate.path().to_path_buf())),
    )
    .unwrap_err();
    assert!(matches!(err, Error::InitBareWithSeparateGitDir));
}

#[test]
fn init_reinit_rediscovers_gitfile_without_separate_flag() {
    let wt = TempDir::new().expect("tempdir");
    let separate = TempDir::new().expect("tempdir");
    Repository::init(
        wt.path(),
        &InitOptions::new()
            .with_initial_branch("main")
            .with_separate_git_dir(Some(separate.path().to_path_buf())),
    )
    .expect("init separate");
    let (_, outcome) =
        Repository::init_with_outcome(wt.path(), &InitOptions::new()).expect("reinit via gitfile");
    assert!(outcome.reinitialized);
    assert_eq!(outcome.initial_branch, "main");
    let gitfile = fs::read_to_string(wt.path().join(".git")).expect("gitfile");
    assert!(gitfile.starts_with("gitdir:"));
}

#[test]
fn reinit_allows_detached_head() {
    let td = TempDir::new().expect("tempdir");
    let repo =
        Repository::init(td.path(), &InitOptions::new().with_initial_branch("main")).expect("init");
    fs::write(
        repo.git_dir().join("HEAD"),
        "deadbeefdeadbeefdeadbeefdeadbeefdeadbeef\n",
    )
    .expect("write detached HEAD");
    let (_, outcome) =
        Repository::init_with_outcome(td.path(), &InitOptions::new()).expect("reinit detached");
    assert!(outcome.reinitialized);
    assert_eq!(outcome.initial_branch, "deadbee");
}

#[test]
fn grit_opens_git_init_sha256_repo() {
    let td = TempDir::new().expect("tempdir");
    let status = Command::new("git")
        .current_dir(td.path())
        .args(["init", "-b", "main", "--object-format=sha256"])
        .env("GIT_CONFIG_GLOBAL", "/dev/null")
        .env("GIT_CONFIG_SYSTEM", "/dev/null")
        .status()
        .expect("git init");
    assert!(status.success());
    let repo = Repository::open(
        &td.path().join(".git"),
        &grit_lib::config::environment::RepositoryOptions::empty()
            .with_work_tree(td.path().to_path_buf()),
    )
    .expect("open git-init sha256 repo");
    assert_eq!(repo.odb().hash_algo(), HashAlgo::Sha256);
}