nils-git-cli 0.7.3

CLI crate for nils-git-cli in the nils-cli workspace.
Documentation
use crate::common;
use common::{GitCliHarness, init_repo};
use nils_test_support::git::{commit_file, git};
use std::fs;
use std::path::Path;

fn copy_staged_help() -> &'static str {
    "Usage: git-cli utils copy-staged [-p|--stdout|--both]\n  -p, --stdout, --print   Print staged diff to stdout (no status message)\n  --both                  Print to stdout and copy to clipboard\n"
}

fn trim_trailing_newlines(input: &str) -> &str {
    input.trim_end_matches(['\n', '\r'])
}

fn staged_diff(repo: &Path) -> String {
    trim_trailing_newlines(&git(repo, &["diff", "--cached", "--no-color"])).to_string()
}

fn repo_root(repo: &Path) -> String {
    trim_trailing_newlines(&git(repo, &["rev-parse", "--show-toplevel"])).to_string()
}

fn shell_escape(value: &str) -> String {
    if value.is_empty() {
        return "''".to_string();
    }
    let mut out = String::from("'");
    for ch in value.chars() {
        if ch == '\'' {
            out.push_str("'\\''");
        } else {
            out.push(ch);
        }
    }
    out.push('\'');
    out
}

fn setup_repo_with_staged_change() -> tempfile::TempDir {
    let dir = init_repo();
    commit_file(dir.path(), "hello.txt", "base\n", "add hello");
    fs::write(dir.path().join("hello.txt"), "base\nchange\n").expect("write staged file");
    git(dir.path(), &["add", "hello.txt"]);
    dir
}

#[test]
fn utils_zip_creates_backup_zip() {
    let harness = GitCliHarness::new();
    let dir = init_repo();

    let short_raw = git(dir.path(), &["rev-parse", "--short", "HEAD"]);
    let short = trim_trailing_newlines(&short_raw);
    let output = harness.run(dir.path(), &["utils", "zip"]);

    assert_eq!(output.code, 0);
    assert_eq!(output.stdout_text(), "");
    assert_eq!(output.stderr_text(), "");

    let zip_path = dir.path().join(format!("backup-{short}.zip"));
    assert!(zip_path.exists(), "expected zip archive to exist");
}

#[test]
fn utils_copy_staged_both_outputs_diff_and_status() {
    let harness = GitCliHarness::new();
    let dir = setup_repo_with_staged_change();
    let diff = staged_diff(dir.path());

    let output = harness.run(dir.path(), &["utils", "copy-staged", "--both"]);

    assert_eq!(output.code, 0);
    assert_eq!(output.stderr_text(), "");
    assert_eq!(
        output.stdout_text(),
        format!("{diff}\n✅ Staged diff copied to clipboard\n")
    );
}

#[test]
fn utils_copy_staged_stdout_outputs_diff_only() {
    let harness = GitCliHarness::new();
    let dir = setup_repo_with_staged_change();
    let diff = staged_diff(dir.path());

    let output = harness.run(dir.path(), &["utils", "copy-staged", "--stdout"]);

    assert_eq!(output.code, 0);
    assert_eq!(output.stderr_text(), "");
    assert_eq!(output.stdout_text(), format!("{diff}\n"));
}

#[test]
fn utils_copy_staged_no_changes_warns_and_exits_1() {
    let harness = GitCliHarness::new();
    let dir = init_repo();

    let output = harness.run(dir.path(), &["utils", "copy-staged"]);

    assert_eq!(output.code, 1);
    assert_eq!(output.stderr_text(), "");
    assert_eq!(output.stdout_text(), "⚠️  No staged changes to copy\n");
}

#[test]
fn utils_copy_staged_help_prints_usage() {
    let harness = GitCliHarness::new();
    let dir = init_repo();

    let output = harness.run(dir.path(), &["utils", "copy-staged", "--help"]);

    assert_eq!(output.code, 0);
    assert_eq!(output.stderr_text(), "");
    assert_eq!(output.stdout_text(), copy_staged_help());
}

#[test]
fn utils_copy_staged_rejects_conflicting_modes() {
    let harness = GitCliHarness::new();
    let dir = tempfile::TempDir::new().expect("tempdir");

    let output = harness.run(dir.path(), &["utils", "copy-staged", "--stdout", "--both"]);

    assert_eq!(output.code, 1);
    assert_eq!(output.stdout_text(), "");
    assert_eq!(
        output.stderr_text(),
        "❗ Only one output mode is allowed: --stdout or --both\n"
    );
}

#[test]
fn utils_copy_staged_rejects_unknown_arg() {
    let harness = GitCliHarness::new();
    let dir = tempfile::TempDir::new().expect("tempdir");

    let output = harness.run(dir.path(), &["utils", "copy-staged", "--nope"]);

    assert_eq!(output.code, 1);
    assert_eq!(output.stdout_text(), "");
    assert_eq!(
        output.stderr_text(),
        "❗ Unknown argument: --nope\nUsage: git-cli utils copy-staged [-p|--stdout|--both]\n"
    );
}

#[test]
fn utils_root_prints_message() {
    let harness = GitCliHarness::new();
    let dir = init_repo();
    let nested = dir.path().join("nested/dir");
    fs::create_dir_all(&nested).expect("create nested dir");
    let root = repo_root(dir.path());

    let output = harness.run(&nested, &["utils", "root"]);

    assert_eq!(output.code, 0);
    assert_eq!(output.stderr_text(), "");
    assert_eq!(
        output.stdout_text(),
        format!("\n📁 Jumped to Git root: {root}\n")
    );
}

#[test]
fn utils_root_not_in_repo_errors() {
    let harness = GitCliHarness::new();
    let dir = tempfile::TempDir::new().expect("tempdir");

    let output = harness.run(dir.path(), &["utils", "root"]);

    assert_eq!(output.code, 1);
    assert_eq!(output.stdout_text(), "");
    assert_eq!(output.stderr_text(), "❌ Not in a git repository\n");
}

#[test]
fn utils_root_shell_outputs_cd_command() {
    let harness = GitCliHarness::new();
    let base = tempfile::TempDir::new().expect("tempdir");
    let repo_dir = base.path().join("space repo");
    fs::create_dir_all(&repo_dir).expect("create repo dir");
    git(&repo_dir, &["init", "-q"]);

    let nested = repo_dir.join("nested dir");
    fs::create_dir_all(&nested).expect("create nested dir");
    let root = repo_root(&repo_dir);

    let output = harness.run(&nested, &["utils", "root", "--shell"]);

    assert_eq!(output.code, 0);
    assert_eq!(
        output.stdout_text(),
        format!("cd -- {}\n", shell_escape(&root))
    );
    assert_eq!(
        output.stderr_text(),
        format!("📁 Jumped to Git root: {root}\n")
    );
}

#[test]
fn utils_commit_hash_missing_ref_errors() {
    let harness = GitCliHarness::new();
    let dir = tempfile::TempDir::new().expect("tempdir");

    let output = harness.run(dir.path(), &["utils", "commit-hash"]);

    assert_eq!(output.code, 1);
    assert_eq!(output.stdout_text(), "");
    assert_eq!(output.stderr_text(), "❌ Missing git ref\n");
}

#[test]
fn utils_commit_hash_outputs_sha_for_head() {
    let harness = GitCliHarness::new();
    let dir = init_repo();
    let expected =
        trim_trailing_newlines(&git(dir.path(), &["rev-parse", "HEAD^{commit}"])).to_string();

    let output = harness.run(dir.path(), &["utils", "commit-hash", "HEAD"]);

    assert_eq!(output.code, 0);
    assert_eq!(output.stderr_text(), "");
    assert_eq!(output.stdout_text(), format!("{expected}\n"));
}

#[test]
fn utils_commit_hash_resolves_annotated_tag() {
    let harness = GitCliHarness::new();
    let dir = init_repo();
    git(dir.path(), &["tag", "-a", "-m", "v1", "v1"]);
    let expected =
        trim_trailing_newlines(&git(dir.path(), &["rev-parse", "HEAD^{commit}"])).to_string();

    let output = harness.run(dir.path(), &["utils", "commit-hash", "v1"]);

    assert_eq!(output.code, 0);
    assert_eq!(output.stderr_text(), "");
    assert_eq!(output.stdout_text(), format!("{expected}\n"));
}