sem-cli 0.9.0

Semantic version control CLI. Shows what entities changed (functions, classes, methods) instead of lines.
use std::fs;
use std::path::Path;
use std::process::Command;

use tempfile::TempDir;

fn git(repo: &Path, args: &[&str]) {
    let output = Command::new("git")
        .current_dir(repo)
        .args(args)
        .output()
        .expect("run git");

    assert!(
        output.status.success(),
        "git {args:?} failed\nstdout: {}\nstderr: {}",
        String::from_utf8_lossy(&output.stdout),
        String::from_utf8_lossy(&output.stderr)
    );
}

fn commit_all(repo: &Path, message: &str) {
    git(repo, &["add", "-A"]);
    git(repo, &["commit", "-q", "-m", message]);
}

fn init_repo() -> TempDir {
    let repo = TempDir::new().expect("create temporary repo");
    git(repo.path(), &["init", "-q"]);
    git(repo.path(), &["config", "user.email", "test@example.com"]);
    git(repo.path(), &["config", "user.name", "Test User"]);
    git(repo.path(), &["config", "commit.gpgsign", "false"]);
    repo
}

fn sem_log_json(repo: &Path, args: &[&str]) -> std::process::Output {
    Command::new(env!("CARGO_BIN_EXE_sem"))
        .current_dir(repo)
        .args(["log"])
        .args(args)
        .args(["--json"])
        .output()
        .expect("run sem log")
}

#[test]
fn log_limit_bounds_history_scan_for_deleted_entities() {
    let repo = init_repo();

    fs::write(repo.path().join("a.py"), "def foo():\n    return 1\n").expect("write a.py");
    commit_all(repo.path(), "add foo");

    fs::write(repo.path().join("a.py"), "def foo():\n    return 2\n").expect("modify a.py");
    commit_all(repo.path(), "modify foo");

    fs::write(repo.path().join("a.py"), "").expect("delete foo");
    commit_all(repo.path(), "delete foo");

    fs::write(repo.path().join("b.py"), "def bar():\n    return 1\n").expect("write b.py");
    commit_all(repo.path(), "add bar");

    fs::write(repo.path().join("c.py"), "def baz():\n    return 1\n").expect("write c.py");
    commit_all(repo.path(), "add baz");

    let output = sem_log_json(repo.path(), &["foo", "--file", "a.py", "--limit", "2"]);
    assert!(
        output.status.success(),
        "sem log failed\nstdout: {}\nstderr: {}",
        String::from_utf8_lossy(&output.stdout),
        String::from_utf8_lossy(&output.stderr)
    );

    let json: serde_json::Value = serde_json::from_slice(&output.stdout).expect("parse json");
    let changes = json["changes"].as_array().expect("changes array");
    assert_eq!(changes.len(), 2);
    assert_eq!(changes[0]["change_type"], "modified (logic)");
    assert_eq!(changes[0]["commit"]["message"], "modify foo");
    assert_eq!(changes[1]["change_type"], "deleted");
    assert_eq!(changes[1]["commit"]["message"], "delete foo");
}

#[test]
fn log_limit_is_not_applied_as_result_entry_truncation() {
    let repo = init_repo();

    fs::write(repo.path().join("a.py"), "def foo():\n    return 1\n").expect("write a.py");
    commit_all(repo.path(), "v1");

    fs::write(repo.path().join("a.py"), "def foo():\n    return 2\n").expect("modify a.py");
    commit_all(repo.path(), "v2");

    fs::write(repo.path().join("a.py"), "def foo():\n    return 3\n").expect("modify a.py");
    commit_all(repo.path(), "v3");

    fs::write(repo.path().join("a.py"), "def foo():\n    return 4\n").expect("modify a.py");
    commit_all(repo.path(), "v4");

    let output = sem_log_json(repo.path(), &["foo", "--file", "a.py", "--limit", "2"]);
    assert!(
        output.status.success(),
        "sem log failed\nstdout: {}\nstderr: {}",
        String::from_utf8_lossy(&output.stdout),
        String::from_utf8_lossy(&output.stderr)
    );

    let json: serde_json::Value = serde_json::from_slice(&output.stdout).expect("parse json");
    let changes = json["changes"].as_array().expect("changes array");
    assert_eq!(changes.len(), 2);
    assert_eq!(changes[0]["change_type"], "modified (logic)");
    assert_eq!(changes[0]["commit"]["message"], "v3");
    assert_eq!(changes[1]["change_type"], "modified (logic)");
    assert_eq!(changes[1]["commit"]["message"], "v4");
}