cargo-changeset 0.1.5

A cargo subcommand for managing changesets
use std::fs;

use predicates::str::contains;
use tempfile::TempDir;

fn create_single_package_project() -> TempDir {
    let dir = TempDir::new().expect("create temp dir");

    fs::write(
        dir.path().join("Cargo.toml"),
        r#"[package]
name = "my-crate"
version = "1.0.0"
edition = "2021"
"#,
    )
    .expect("write Cargo.toml");

    fs::create_dir_all(dir.path().join("src")).expect("create src dir");
    fs::write(dir.path().join("src/lib.rs"), "").expect("write lib.rs");

    fs::create_dir_all(dir.path().join(".changeset/changesets"))
        .expect("create .changeset/changesets dir");

    dir
}

fn create_workspace_project() -> TempDir {
    let dir = TempDir::new().expect("create temp dir");

    fs::write(
        dir.path().join("Cargo.toml"),
        r#"[workspace]
members = ["crates/*"]
resolver = "2"
"#,
    )
    .expect("write workspace Cargo.toml");

    fs::create_dir_all(dir.path().join("crates/crate-a/src")).expect("create crate-a dir");
    fs::write(
        dir.path().join("crates/crate-a/Cargo.toml"),
        r#"[package]
name = "crate-a"
version = "1.0.0"
edition = "2021"
"#,
    )
    .expect("write crate-a Cargo.toml");
    fs::write(dir.path().join("crates/crate-a/src/lib.rs"), "").expect("write lib.rs");

    fs::create_dir_all(dir.path().join("crates/crate-b/src")).expect("create crate-b dir");
    fs::write(
        dir.path().join("crates/crate-b/Cargo.toml"),
        r#"[package]
name = "crate-b"
version = "2.0.0"
edition = "2021"
"#,
    )
    .expect("write crate-b Cargo.toml");
    fs::write(dir.path().join("crates/crate-b/src/lib.rs"), "").expect("write lib.rs");

    fs::create_dir_all(dir.path().join(".changeset/changesets"))
        .expect("create .changeset/changesets dir");

    dir
}

fn create_workspace_with_inherited_versions() -> TempDir {
    let dir = TempDir::new().expect("create temp dir");

    fs::write(
        dir.path().join("Cargo.toml"),
        r#"[workspace]
members = ["crates/*"]
resolver = "2"

[workspace.package]
version = "1.0.0"
edition = "2021"
"#,
    )
    .expect("write workspace Cargo.toml");

    fs::create_dir_all(dir.path().join("crates/crate-a/src")).expect("create crate-a dir");
    fs::write(
        dir.path().join("crates/crate-a/Cargo.toml"),
        r#"[package]
name = "crate-a"
version.workspace = true
edition.workspace = true
"#,
    )
    .expect("write crate-a Cargo.toml");
    fs::write(dir.path().join("crates/crate-a/src/lib.rs"), "").expect("write lib.rs");

    fs::create_dir_all(dir.path().join(".changeset/changesets"))
        .expect("create .changeset/changesets dir");

    dir
}

fn write_changeset(dir: &TempDir, filename: &str, package: &str, bump: &str, summary: &str) {
    let content = format!(
        r#"---
"{package}": {bump}
---

{summary}
"#
    );
    fs::write(
        dir.path().join(".changeset/changesets").join(filename),
        content,
    )
    .expect("write changeset");
}

macro_rules! cargo_changeset_status {
    () => {
        assert_cmd::cargo::cargo_bin_cmd!("cargo-changeset")
    };
}

#[test]
fn status_with_no_changesets() {
    let workspace = create_single_package_project();

    cargo_changeset_status!()
        .arg("status")
        .current_dir(workspace.path())
        .assert()
        .success()
        .stdout(contains("No pending changesets."));
}

#[test]
fn status_shows_single_changeset() {
    let workspace = create_single_package_project();
    write_changeset(&workspace, "fix-bug.md", "my-crate", "patch", "Fix a bug");

    cargo_changeset_status!()
        .arg("status")
        .current_dir(workspace.path())
        .assert()
        .success()
        .stdout(contains("Pending changesets: 1"))
        .stdout(contains("fix-bug.md"))
        .stdout(contains("Projected releases:"))
        .stdout(contains("my-crate: 1.0.0 -> 1.0.1 (patch)"))
        .stdout(contains("Summary: 1 changeset(s), 1 package(s) to release"));
}

#[test]
fn status_shows_multiple_changesets() {
    let workspace = create_single_package_project();
    write_changeset(&workspace, "fix.md", "my-crate", "patch", "Fix bug");
    write_changeset(&workspace, "feature.md", "my-crate", "minor", "Add feature");

    cargo_changeset_status!()
        .arg("status")
        .current_dir(workspace.path())
        .assert()
        .success()
        .stdout(contains("Pending changesets: 2"))
        .stdout(contains("my-crate: 1.0.0 -> 1.1.0 (minor)"))
        .stdout(contains("(from: patch, minor)"));
}

#[test]
fn status_shows_workspace_packages() {
    let workspace = create_workspace_project();
    write_changeset(&workspace, "fix-a.md", "crate-a", "patch", "Fix A");

    cargo_changeset_status!()
        .arg("status")
        .current_dir(workspace.path())
        .assert()
        .success()
        .stdout(contains("crate-a: 1.0.0 -> 1.0.1 (patch)"))
        .stdout(contains("Packages without changesets:"))
        .stdout(contains("crate-b (2.0.0)"));
}

#[test]
fn status_shows_inherited_version_warning() {
    let workspace = create_workspace_with_inherited_versions();

    cargo_changeset_status!()
        .arg("status")
        .current_dir(workspace.path())
        .assert()
        .success()
        .stdout(contains("No pending changesets."))
        .stdout(contains("Warning: Packages with inherited versions:"))
        .stdout(contains("crate-a"))
        .stdout(contains("--convert flag"));
}

#[test]
fn status_shows_inherited_version_warning_with_changesets() {
    let workspace = create_workspace_with_inherited_versions();
    write_changeset(&workspace, "fix.md", "crate-a", "patch", "Fix");

    cargo_changeset_status!()
        .arg("status")
        .current_dir(workspace.path())
        .assert()
        .success()
        .stdout(contains("Pending changesets: 1"))
        .stdout(contains("Warning: Packages with inherited versions:"))
        .stdout(contains("--convert flag"));
}

#[test]
fn status_shows_unknown_package_warning() {
    let workspace = create_single_package_project();
    write_changeset(
        &workspace,
        "fix.md",
        "nonexistent-crate",
        "patch",
        "Fix typo",
    );

    cargo_changeset_status!()
        .arg("status")
        .current_dir(workspace.path())
        .assert()
        .success()
        .stdout(contains("Warning: Unknown packages in changesets:"))
        .stdout(contains("nonexistent-crate"));
}

#[test]
fn status_multiple_packages_multiple_bumps() {
    let workspace = create_workspace_project();
    write_changeset(&workspace, "fix-a.md", "crate-a", "patch", "Fix A");
    write_changeset(&workspace, "feature-a.md", "crate-a", "minor", "Feature A");
    write_changeset(
        &workspace,
        "breaking-b.md",
        "crate-b",
        "major",
        "Breaking B",
    );

    cargo_changeset_status!()
        .arg("status")
        .current_dir(workspace.path())
        .assert()
        .success()
        .stdout(contains("Pending changesets: 3"))
        .stdout(contains(
            "crate-a: 1.0.0 -> 1.1.0 (minor) (from: patch, minor)",
        ))
        .stdout(contains("crate-b: 2.0.0 -> 3.0.0 (major)"))
        .stdout(contains("Summary: 3 changeset(s), 2 package(s) to release"));
}