simit 0.17.6

Semver-aware git commit helper for Rust projects
Documentation
use std::fs;
use std::path::{Path, PathBuf};
use std::process::Command;

use tempfile::TempDir;

mod common;

fn simit() -> Command {
    common::simit()
}

fn init_package(version: &str, arch_config: &str) -> TempDir {
    let temp = TempDir::new().unwrap();
    let root = temp.path();

    fs::write(
        root.join("Cargo.toml"),
        format!(
            r#"[package]
name = "demo-app"
version = "{version}"
edition = "2024"
rust-version = "1.85"
license = "MIT"
description = "Demo application"
homepage = "https://example.com/demo"
"#
        ),
    )
    .unwrap();
    fs::create_dir(root.join("src")).unwrap();
    fs::write(root.join("src/main.rs"), "fn main() {}\n").unwrap();
    fs::write(
        root.join("simit.toml"),
        format!(
            r#"[scoop]
name = "demo-app"
bucket_url = "https://codeberg.org/example/scoop-demo-app.git"
download_repo = "example/demo-app"
binaries = ["demo-app"]
{arch_config}
"#
        ),
    )
    .unwrap();

    temp
}

fn fixture_archives(root: &Path) -> Vec<(String, PathBuf)> {
    [
        ("x64", b"x64\n".as_slice()),
        ("arm64", b"arm64\n".as_slice()),
    ]
    .into_iter()
    .map(|(architecture, content)| {
        let path = root.join(format!("{architecture}.zip"));
        fs::write(&path, content).unwrap();
        (architecture.to_owned(), path)
    })
    .collect()
}

fn archive_args(archives: &[(String, PathBuf)]) -> Vec<String> {
    archives
        .iter()
        .flat_map(|(architecture, path)| {
            vec![
                "--archive".to_owned(),
                format!("{architecture}={}", path.display()),
            ]
        })
        .collect()
}

fn read(path: &Path) -> String {
    fs::read_to_string(path).unwrap_or_else(|err| panic!("reading {}: {err}", path.display()))
}

fn sha256(path: &Path) -> String {
    simit::sha256::sha256_of_file(path).unwrap()
}

fn git(dir: &Path, args: &[&str]) {
    let status = Command::new("git")
        .arg("-C")
        .arg(dir)
        .args(args)
        .status()
        .unwrap();
    assert!(status.success(), "git {}", args.join(" "));
}

#[test]
fn render_version_outputs_stable_manifest() {
    let temp = init_package("0.9.0", "");

    let output = simit()
        .current_dir(temp.path())
        .args(["dist", "scoop", "render", "--version", "1.2.3"])
        .output()
        .unwrap();

    assert!(output.status.success());
    let stdout = String::from_utf8(output.stdout).unwrap();
    serde_json::from_str::<serde_json::Value>(&stdout).unwrap();
    assert_eq!(
        stdout,
        r#"{
  "version": "1.2.3",
  "description": "Demo application",
  "homepage": "https://example.com/demo",
  "license": "MIT",
  "architecture": {
    "64bit": {
      "url": "https://codeberg.org/example/demo-app/releases/download/1.2.3/demo-app-1.2.3-x86_64-windows.zip",
      "hash": "0000000000000000000000000000000000000000000000000000000000000000",
      "bin": "demo-app"
    },
    "arm64": {
      "url": "https://codeberg.org/example/demo-app/releases/download/1.2.3/demo-app-1.2.3-aarch64-windows.zip",
      "hash": "0000000000000000000000000000000000000000000000000000000000000000",
      "bin": "demo-app"
    }
  }
}
"#
    );
}

#[test]
fn bump_writes_manifest_with_real_sha256s() {
    let temp = init_package("0.9.0", "");
    let bucket = temp.path().join("bucket-repo");
    let archives = fixture_archives(temp.path());
    let mut args = vec![
        "dist".to_owned(),
        "scoop".to_owned(),
        "bump".to_owned(),
        "--version".to_owned(),
        "1.2.3".to_owned(),
        "--bucket".to_owned(),
        bucket.display().to_string(),
    ];
    args.extend(archive_args(&archives));

    let status = simit()
        .current_dir(temp.path())
        .args(args)
        .status()
        .unwrap();

    assert!(status.success());
    let manifest = read(&bucket.join("bucket/demo-app.json"));
    serde_json::from_str::<serde_json::Value>(&manifest).unwrap();
    for (_, path) in archives {
        assert!(manifest.contains(&format!(r#""hash": "{}""#, sha256(&path))));
    }
}

#[test]
fn bump_errors_when_enabled_architecture_archive_is_missing() {
    let temp = init_package("0.9.0", "");
    let bucket = temp.path().join("bucket-repo");
    let archive = temp.path().join("x64.zip");
    fs::write(&archive, b"x64\n").unwrap();

    let output = simit()
        .current_dir(temp.path())
        .args([
            "dist",
            "scoop",
            "bump",
            "--version",
            "1.2.3",
            "--bucket",
            bucket.to_str().unwrap(),
            "--archive",
            &format!("x64={}", archive.display()),
        ])
        .output()
        .unwrap();

    assert!(!output.status.success());
    let stderr = String::from_utf8(output.stderr).unwrap();
    assert!(stderr.contains("missing --archive for arm64"));
}

#[test]
fn render_no_arch_arm64_omits_arm64_block() {
    let temp = init_package("0.9.0", "");

    let output = simit()
        .current_dir(temp.path())
        .args([
            "dist",
            "scoop",
            "render",
            "--version",
            "1.2.3",
            "--scoop-no-arch",
            "arm64",
        ])
        .output()
        .unwrap();

    assert!(output.status.success());
    let stdout = String::from_utf8(output.stdout).unwrap();
    assert!(stdout.contains(r#""64bit""#));
    assert!(!stdout.contains(r#""arm64""#));
    assert!(!stdout.contains("aarch64-windows.zip"));
}

#[test]
fn init_scoop_bucket_print_matches_render() {
    let temp = init_package("0.9.0", "");

    let render = simit()
        .current_dir(temp.path())
        .args(["dist", "scoop", "render"])
        .output()
        .unwrap();
    let init = simit()
        .current_dir(temp.path())
        .args(["init", "scoop-bucket", "--target", "/tmp/unused", "--print"])
        .output()
        .unwrap();

    assert!(render.status.success());
    assert!(init.status.success());
    assert_eq!(render.stdout, init.stdout);
}

#[test]
fn bump_without_push_writes_expected_git_diff() {
    let temp = init_package("0.9.0", "");
    let bucket = temp.path().join("bucket-repo");
    fs::create_dir(&bucket).unwrap();
    assert!(
        Command::new("git")
            .arg("-C")
            .arg(&bucket)
            .arg("init")
            .status()
            .unwrap()
            .success()
    );

    let archives = fixture_archives(temp.path());
    let mut args = vec![
        "dist".to_owned(),
        "scoop".to_owned(),
        "bump".to_owned(),
        "--version".to_owned(),
        "1.2.3".to_owned(),
        "--bucket".to_owned(),
        bucket.display().to_string(),
    ];
    args.extend(archive_args(&archives));

    let status = simit()
        .current_dir(temp.path())
        .args(args)
        .status()
        .unwrap();

    assert!(status.success());
    let git_status = Command::new("git")
        .arg("-C")
        .arg(&bucket)
        .args(["status", "--porcelain"])
        .output()
        .unwrap();
    assert!(git_status.status.success());
    assert_eq!(
        String::from_utf8(git_status.stdout).unwrap(),
        "?? bucket/\n"
    );
}

#[test]
fn bump_can_clone_commit_and_push_bucket_repo() {
    let temp = init_package(
        "0.9.0",
        r#"[scoop.architectures]
arm64 = false
"#,
    );
    let remote = temp.path().join("remote.git");
    let seed = temp.path().join("seed");
    fs::create_dir(&remote).unwrap();
    fs::create_dir(&seed).unwrap();
    git(&remote, &["init", "--bare", "--initial-branch", "main"]);
    git(&seed, &["init", "--initial-branch", "main"]);
    fs::create_dir(seed.join("bucket")).unwrap();
    fs::write(seed.join("bucket/demo-app.json"), "{}\n").unwrap();
    git(&seed, &["config", "user.email", "test@example.com"]);
    git(&seed, &["config", "user.name", "test"]);
    git(&seed, &["add", "bucket/demo-app.json"]);
    git(&seed, &["commit", "-m", "seed"]);
    git(
        &seed,
        &["remote", "add", "origin", remote.to_str().unwrap()],
    );
    git(&seed, &["push", "origin", "HEAD:main"]);

    let archives = fixture_archives(temp.path());
    let x64 = archives
        .iter()
        .find(|(architecture, _)| architecture == "x64")
        .unwrap();
    let output = simit()
        .current_dir(temp.path())
        .env("SCOOP_BUCKET_TOKEN", "local-token")
        .args([
            "dist",
            "scoop",
            "bump",
            "--version",
            "1.2.3",
            "--archive",
            &format!("x64={}", x64.1.display()),
            "--bucket-url",
            remote.to_str().unwrap(),
            "--work-dir",
            temp.path().join("work").to_str().unwrap(),
            "--push",
        ])
        .output()
        .unwrap();

    assert!(
        output.status.success(),
        "{}",
        String::from_utf8_lossy(&output.stderr)
    );

    let verify = temp.path().join("verify");
    let status = Command::new("git")
        .args(["clone", remote.to_str().unwrap(), verify.to_str().unwrap()])
        .status()
        .unwrap();
    assert!(status.success());
    let manifest = read(&verify.join("bucket/demo-app.json"));
    assert!(manifest.contains(r#""version": "1.2.3""#));
    assert!(manifest.contains(&sha256(&x64.1)));
}