git-iris 2.1.0

AI-powered Git workflow assistant for smart commits, code reviews, changelogs, and release notes
Documentation
use std::fs;
use std::process::Command;

use rig::tool::Tool;
use tempfile::TempDir;

use crate::agents::tools::git::{GitShow, GitShowArgs};
use crate::agents::tools::with_active_repo_root;

fn run_git(temp_dir: &TempDir, args: &[&str]) {
    let output = Command::new("git")
        .args(args)
        .current_dir(temp_dir.path())
        .output()
        .expect("git command should run");
    assert!(
        output.status.success(),
        "git {:?} failed: {}",
        args,
        String::from_utf8_lossy(&output.stderr)
    );
}

fn repo_with_commits() -> TempDir {
    let temp_dir = TempDir::new().expect("temp dir should be created");
    run_git(&temp_dir, &["init"]);
    run_git(&temp_dir, &["config", "user.name", "Iris Tester"]);
    run_git(&temp_dir, &["config", "user.email", "iris@example.com"]);
    run_git(&temp_dir, &["config", "commit.gpgsign", "false"]);
    run_git(&temp_dir, &["config", "tag.gpgsign", "false"]);

    fs::create_dir_all(temp_dir.path().join("src")).expect("src dir should be created");
    fs::write(temp_dir.path().join("src/lib.rs"), "pub fn first() {}\n")
        .expect("source file should be written");
    fs::write(temp_dir.path().join("src/old.rs"), "pub fn old() {}\n")
        .expect("old source file should be written");
    fs::write(temp_dir.path().join("README.md"), "# Demo\n").expect("README should be written");
    run_git(&temp_dir, &["add", "."]);
    run_git(&temp_dir, &["commit", "-m", "Add initial files"]);

    fs::write(
        temp_dir.path().join("src/lib.rs"),
        "pub fn first() {}\npub fn second() {}\n",
    )
    .expect("source file should be updated");
    fs::write(temp_dir.path().join("README.md"), "# Demo\n\nUpdated\n")
        .expect("README should be updated");
    fs::remove_file(temp_dir.path().join("src/old.rs")).expect("old source file should be removed");
    run_git(&temp_dir, &["add", "."]);
    run_git(&temp_dir, &["commit", "-m", "Extend demo files"]);

    temp_dir
}

#[tokio::test]
async fn git_show_returns_commit_patch_and_metadata() {
    let temp_dir = repo_with_commits();

    let output = with_active_repo_root(temp_dir.path(), async {
        GitShow
            .call(GitShowArgs {
                commit: "HEAD".to_string(),
                files: None,
                max_output_chars: 20_000,
            })
            .await
            .expect("git show should succeed")
    })
    .await;

    assert!(output.contains("Git show for HEAD"));
    assert!(output.contains("Author:"));
    assert!(output.contains("Commit:"));
    assert!(output.contains("Extend demo files"));
    assert!(output.contains("+pub fn second() {}"));
}

#[tokio::test]
async fn git_show_filters_paths() {
    let temp_dir = repo_with_commits();

    let output = with_active_repo_root(temp_dir.path(), async {
        GitShow
            .call(GitShowArgs {
                commit: "HEAD".to_string(),
                files: Some(vec!["src/lib.rs".into()]),
                max_output_chars: 20_000,
            })
            .await
            .expect("git show should succeed")
    })
    .await;

    assert!(output.contains("Filtered paths: src/lib.rs"));
    assert!(output.contains("+pub fn second() {}"));
    assert!(!output.contains("README.md"));
}

#[tokio::test]
async fn git_show_filters_historical_paths_missing_from_worktree() {
    let temp_dir = repo_with_commits();

    let output = with_active_repo_root(temp_dir.path(), async {
        GitShow
            .call(GitShowArgs {
                commit: "HEAD^".to_string(),
                files: Some(vec!["src/old.rs".into()]),
                max_output_chars: 20_000,
            })
            .await
            .expect("historical paths should not need to exist in the worktree")
    })
    .await;

    assert!(output.contains("Filtered paths: src/old.rs"));
    assert!(output.contains("+pub fn old() {}"));
}

#[tokio::test]
async fn git_show_rejects_option_like_commits() {
    let temp_dir = repo_with_commits();

    let error = with_active_repo_root(temp_dir.path(), async {
        GitShow
            .call(GitShowArgs {
                commit: "--help".to_string(),
                files: None,
                max_output_chars: 20_000,
            })
            .await
            .expect_err("option-like refs should be rejected")
    })
    .await;

    assert!(error.to_string().contains("commit, tag, or branch"));
}

#[tokio::test]
async fn git_show_rejects_parent_directory_paths() {
    let temp_dir = repo_with_commits();

    let error = with_active_repo_root(temp_dir.path(), async {
        GitShow
            .call(GitShowArgs {
                commit: "HEAD".to_string(),
                files: Some(vec!["../README.md".into()]),
                max_output_chars: 20_000,
            })
            .await
            .expect_err("parent paths should be rejected")
    })
    .await;

    assert!(error.to_string().contains("repository-relative"));
}