//! Integration tests for the `gitshape` command. use std::fs; use std::process::Command as ProcessCommand; use assert_cmd::Command; use predicates::prelude::*; use tempfile::tempdir; fn cargo_command() -> Command { Command::cargo_bin("gitshape").expect("binary") } #[test] fn status_subcommand_summarizes_dirty_repository_as_json() { let temp = tempdir().expect("tempdir"); run_git(temp.path(), ["-c", "init.defaultBranch=main", "init"]); run_git(temp.path(), ["config", "user.name", "Codex"]); run_git(temp.path(), ["config", "user.email", "codex@example.com"]); fs::write(temp.path().join("tracked.txt"), "first\n").expect("tracked"); run_git(temp.path(), ["add", "tracked.txt"]); run_git(temp.path(), ["commit", "-m", "init"]); fs::write(temp.path().join("tracked.txt"), "second\n").expect("modify"); fs::write(temp.path().join("new.txt"), "new\n").expect("untracked"); let mut command = cargo_command(); command .arg("--json") .arg("status") .arg("--repo") .arg(temp.path()) .assert() .success() .stdout(predicate::str::contains("\"modified\":1")) .stdout(predicate::str::contains("\"untracked\":1")) .stdout(predicate::str::contains("\"clean\":false")); } #[test] fn diff_subcommand_reports_staged_change_summary() { let temp = tempdir().expect("tempdir"); run_git(temp.path(), ["init"]); run_git(temp.path(), ["config", "user.name", "Codex"]); run_git(temp.path(), ["config", "user.email", "codex@example.com"]); fs::write( temp.path().join("src.rs"), "fn build() {\n println!(\"old\");\n}\n", ) .expect("src"); run_git(temp.path(), ["add", "src.rs"]); run_git(temp.path(), ["commit", "-m", "init"]); fs::write( temp.path().join("src.rs"), "fn build() {\n println!(\"new\");\n}\n", ) .expect("modify"); run_git(temp.path(), ["add", "src.rs"]); let mut command = cargo_command(); command .arg("--json") .arg("diff") .arg("--repo") .arg(temp.path()) .arg("--staged") .assert() .success() .stdout(predicate::str::contains("\"mode\":\"staged\"")) .stdout(predicate::str::contains("\"files_changed\":1")) .stdout(predicate::str::contains("\"status\":\"modified\"")); } #[test] fn status_subcommand_handles_unborn_repository() { let temp = tempdir().expect("tempdir"); run_git(temp.path(), ["-c", "init.defaultBranch=main", "init"]); let mut command = cargo_command(); command .arg("--json") .arg("status") .arg("--repo") .arg(temp.path()) .assert() .success() .stdout(predicate::str::contains("\"branch\":\"main\"")) .stdout(predicate::str::contains("\"head\":\"-\"")) .stdout(predicate::str::contains("\"clean\":true")); } #[test] fn diff_subcommand_accepts_explicit_revisions() { let temp = tempdir().expect("tempdir"); run_git(temp.path(), ["init"]); run_git(temp.path(), ["config", "user.name", "Codex"]); run_git(temp.path(), ["config", "user.email", "codex@example.com"]); fs::write(temp.path().join("demo.txt"), "one\n").expect("demo"); run_git(temp.path(), ["add", "demo.txt"]); run_git(temp.path(), ["commit", "-m", "first"]); let first = run_git_capture(temp.path(), ["rev-parse", "--short", "HEAD"]); fs::write(temp.path().join("demo.txt"), "one\ntwo\n").expect("update"); run_git(temp.path(), ["add", "demo.txt"]); run_git(temp.path(), ["commit", "-m", "second"]); let second = run_git_capture(temp.path(), ["rev-parse", "--short", "HEAD"]); let mut command = cargo_command(); command .arg("--json") .arg("diff") .arg("--repo") .arg(temp.path()) .arg(first) .arg(second) .assert() .success() .stdout(predicate::str::contains("\"mode\":\"revisions\"")) .stdout(predicate::str::contains("\"revisions\":[")) .stdout(predicate::str::contains("\"files_changed\":1")); } #[test] fn diff_subcommand_returns_success_for_empty_revision_diff() { let temp = tempdir().expect("tempdir"); run_git(temp.path(), ["init"]); run_git(temp.path(), ["config", "user.name", "Codex"]); run_git(temp.path(), ["config", "user.email", "codex@example.com"]); fs::write(temp.path().join("demo.txt"), "one\n").expect("demo"); run_git(temp.path(), ["add", "demo.txt"]); run_git(temp.path(), ["commit", "-m", "first"]); let mut command = cargo_command(); command .arg("diff") .arg("--repo") .arg(temp.path()) .arg("HEAD") .arg("HEAD") .assert() .success() .stdout(predicate::str::contains("mode=revisions")) .stdout(predicate::str::contains("files=0")) .stdout(predicate::str::contains("line_additions=0")) .stdout(predicate::str::contains("line_deletions=0")); } #[test] fn help_includes_new_subcommands_and_flags() { let mut command = cargo_command(); command .arg("--help") .assert() .success() .stdout(predicate::str::contains("gitshape [OPTIONS] status")) .stdout(predicate::str::contains("gitshape [OPTIONS] diff")) .stdout(predicate::str::contains("--repo")) .stdout(predicate::str::contains("--staged")) .stdout(predicate::str::contains("--ignored")) .stdout(predicate::str::contains("gitshape")); } #[test] fn bare_path_defaults_to_status_mode() { let temp = tempdir().expect("tempdir"); run_git(temp.path(), ["-c", "init.defaultBranch=main", "init"]); let mut command = cargo_command(); command .arg("--json") .arg(temp.path()) .assert() .success() .stdout(predicate::str::contains("\"repository_present\":true")) .stdout(predicate::str::contains("\"branch\":\"main\"")); } #[test] fn status_reports_missing_repository_without_failing() { let temp = tempdir().expect("tempdir"); let mut command = cargo_command(); command .arg("--json") .arg("status") .arg("--repo") .arg(temp.path()) .assert() .success() .stdout(predicate::str::contains("\"repository_present\":false")) .stdout(predicate::str::contains( "\"error\":\"fatal: not a git repository", )); } #[test] fn diff_subcommand_rejects_staged_with_explicit_revisions() { let mut command = cargo_command(); command .arg("diff") .arg("--staged") .arg("HEAD") .assert() .failure() .stderr(predicate::str::contains( "diff --staged does not accept explicit revisions", )); } #[test] fn status_subcommand_rejects_repo_and_positional_path_together() { let temp = tempdir().expect("tempdir"); let mut command = cargo_command(); command .arg("status") .arg("--repo") .arg(temp.path()) .arg(temp.path()) .assert() .failure() .stderr(predicate::str::contains( "status accepts either --repo or positional [PATH], not both", )); } #[test] fn diff_subcommand_text_output_renders_rename_details() { let temp = tempdir().expect("tempdir"); run_git(temp.path(), ["init"]); run_git(temp.path(), ["config", "user.name", "Codex"]); run_git(temp.path(), ["config", "user.email", "codex@example.com"]); fs::write( temp.path().join("old_name.rs"), "fn rename_me() {\n println!(\"old\");\n}\n", ) .expect("old file"); run_git(temp.path(), ["add", "old_name.rs"]); run_git(temp.path(), ["commit", "-m", "before rename"]); let first = run_git_capture(temp.path(), ["rev-parse", "--short", "HEAD"]); run_git(temp.path(), ["mv", "old_name.rs", "new_name.rs"]); fs::write( temp.path().join("new_name.rs"), "fn rename_me() {\n println!(\"old\");\n}\nfn helper() {}\n", ) .expect("new file"); run_git(temp.path(), ["add", "new_name.rs"]); run_git(temp.path(), ["commit", "-m", "after rename"]); let second = run_git_capture(temp.path(), ["rev-parse", "--short", "HEAD"]); let mut command = cargo_command(); command .arg("diff") .arg("--repo") .arg(temp.path()) .arg(first) .arg(second) .assert() .success() .stdout(predicate::str::contains("mode=revisions")) .stdout(predicate::str::contains("status=renamed")) .stdout(predicate::str::contains("path=new_name.rs")) .stdout(predicate::str::contains("previous=old_name.rs")); } fn run_git(cwd: &std::path::Path, args: [&str; N]) { let status = ProcessCommand::new("git") .current_dir(cwd) .args(args) .status() .expect("git command"); assert!(status.success(), "git command failed"); } fn run_git_capture(cwd: &std::path::Path, args: [&str; N]) -> String { let output = ProcessCommand::new("git") .current_dir(cwd) .args(args) .output() .expect("git command output"); assert!(output.status.success(), "git command failed"); String::from_utf8(output.stdout) .expect("utf8") .trim() .to_string() }