rustytag 1.0.21

A semantic version management tool built on Git tags.
Documentation
use serde_json::Value;
use std::fs;
use std::path::{Path, PathBuf};
use std::process::{Command, Output};
use std::sync::atomic::{AtomicU64, Ordering};

static NEXT_REPO_ID: AtomicU64 = AtomicU64::new(0);

struct TestRepo(PathBuf);

impl TestRepo {
    fn new() -> Self {
        let id = NEXT_REPO_ID.fetch_add(1, Ordering::Relaxed);
        let path = std::env::temp_dir().join(format!("rustytag-show-{}-{id}", std::process::id()));

        fs::create_dir_all(&path).unwrap();
        run_git(&path, &["init", "--quiet"]);
        run_git(&path, &["config", "user.name", "RustyTag Test"]);
        run_git(&path, &["config", "user.email", "rustytag@example.com"]);
        fs::write(path.join("README.md"), "test repository\n").unwrap();
        run_git(&path, &["add", "README.md"]);
        run_git(&path, &["commit", "--quiet", "-m", "Initial commit"]);

        Self(path)
    }

    fn run_rustytag(&self, args: &[&str]) -> Output {
        Command::new(env!("CARGO_BIN_EXE_rustytag"))
            .args(args)
            .current_dir(&self.0)
            .env("HOME", self.0.join("empty-home"))
            .output()
            .unwrap()
    }

    fn run_rustytag_with_home(&self, args: &[&str], home: &Path) -> Output {
        Command::new(env!("CARGO_BIN_EXE_rustytag"))
            .args(args)
            .current_dir(&self.0)
            .env("HOME", home)
            .output()
            .unwrap()
    }
}

impl Drop for TestRepo {
    fn drop(&mut self) {
        fs::remove_dir_all(&self.0).unwrap();
    }
}

fn run_git(repo: &Path, args: &[&str]) {
    let output = Command::new("git")
        .args(["-C", repo.to_str().unwrap()])
        .args(args)
        .output()
        .unwrap();
    assert!(
        output.status.success(),
        "git command failed: {}",
        String::from_utf8_lossy(&output.stderr)
    );
}

#[test]
fn show_machine_output_is_clean_in_untagged_repo() {
    let repo = TestRepo::new();

    let human = repo.run_rustytag(&["show"]);
    assert!(human.status.success());
    let human_stdout = String::from_utf8(human.stdout).unwrap();
    assert!(human_stdout.contains("No tags found, using initial version"));
    assert!(human_stdout.contains("Current Version: 0.1.0"));

    let quiet = repo.run_rustytag(&["show", "--quiet"]);
    assert!(quiet.status.success());
    assert_eq!(String::from_utf8(quiet.stdout).unwrap(), "0.1.0\n");
    assert!(quiet.stderr.is_empty());

    let json = repo.run_rustytag(&["show", "--json"]);
    assert!(json.status.success());
    assert!(json.stderr.is_empty());

    let output: Value = serde_json::from_slice(&json.stdout).unwrap();
    assert_eq!(output["version"], "0.1.0");
    assert_eq!(output["commit_count"], 1);
    assert_eq!(output["repository_url"], Value::Null);
    assert_eq!(output["rustytag_version"], env!("CARGO_PKG_VERSION"));
    assert!(output["branch"].is_string());
}

#[test]
fn show_rejects_json_and_quiet_together() {
    let output = Command::new(env!("CARGO_BIN_EXE_rustytag"))
        .args(["show", "--json", "--quiet"])
        .output()
        .unwrap();

    assert!(!output.status.success());
    assert!(String::from_utf8_lossy(&output.stderr).contains("cannot be used with"));
}

#[test]
fn user_prefix_is_applied_and_saved_to_repository_config() {
    let repo = TestRepo::new();
    let home = repo.0.join("home");
    fs::create_dir_all(&home).unwrap();
    fs::write(home.join(".rustytag.json"), r#"{"version_prefix":"v"}"#).unwrap();

    let output = repo.run_rustytag_with_home(&["show", "--quiet"], &home);

    assert!(output.status.success());
    assert_eq!(String::from_utf8(output.stdout).unwrap(), "v0.1.0\n");
    let repository_config: Value =
        serde_json::from_slice(&fs::read(repo.0.join(".rustytag.json")).unwrap()).unwrap();
    assert_eq!(repository_config["version_prefix"], "v");
}

#[test]
fn detected_tag_prefix_is_saved_to_repository_config() {
    let repo = TestRepo::new();
    run_git(&repo.0, &["tag", "v0.1.0"]);

    let output = repo.run_rustytag(&["show", "--quiet"]);

    assert!(output.status.success());
    assert_eq!(String::from_utf8(output.stdout).unwrap(), "v0.1.0\n");
    let repository_config: Value =
        serde_json::from_slice(&fs::read(repo.0.join(".rustytag.json")).unwrap()).unwrap();
    assert_eq!(repository_config["version_prefix"], "v");
}

#[test]
fn explicit_version_prefix_is_committed_with_the_tag() {
    let repo = TestRepo::new();
    run_git(
        &repo.0,
        &[
            "remote",
            "add",
            "origin",
            "https://github.com/example/project.git",
        ],
    );

    let output = repo.run_rustytag(&["patch", "--version", "v0.1.0"]);

    assert!(
        output.status.success(),
        "rustytag failed: {}",
        String::from_utf8_lossy(&output.stderr)
    );
    let tags = git_stdout(&repo.0, &["tag", "--list"]);
    assert_eq!(tags, "v0.1.0\n");
    let repository_config: Value =
        serde_json::from_slice(&fs::read(repo.0.join(".rustytag.json")).unwrap()).unwrap();
    assert_eq!(repository_config["version_prefix"], "v");
    assert_eq!(repository_config["version"], "0.1.0");
    assert!(git_stdout(&repo.0, &["status", "--porcelain"]).is_empty());
}

#[test]
fn configured_prefix_is_saved_and_used_by_the_repository() {
    let repo = TestRepo::new();

    let configured = repo.run_rustytag(&["config", "--set", "VERSION_PREFIX=v"]);
    assert!(configured.status.success());

    let shown = repo.run_rustytag(&["show", "--quiet"]);
    assert!(shown.status.success());
    assert_eq!(String::from_utf8(shown.stdout).unwrap(), "v0.1.0\n");
    let repository_config: Value =
        serde_json::from_slice(&fs::read(repo.0.join(".rustytag.json")).unwrap()).unwrap();
    assert_eq!(repository_config["version_prefix"], "v");
}

fn git_stdout(repo: &Path, args: &[&str]) -> String {
    let output = Command::new("git")
        .args(["-C", repo.to_str().unwrap()])
        .args(args)
        .output()
        .unwrap();
    assert!(
        output.status.success(),
        "git command failed: {}",
        String::from_utf8_lossy(&output.stderr)
    );
    String::from_utf8(output.stdout).unwrap()
}