cargo-version-info 0.0.16

Cargo subcommand for unified version management across CI/CD, Rust code, and shell scripts
Documentation
//! Get current version from Cargo.toml command.
//!
//! This command extracts the version from a Cargo.toml file, checking both
//! `[workspace.package]` and `[package]` sections.
//!
//! # Examples
//!
//! ```bash
//! # Get version from current directory's Cargo.toml
//! cargo version-info current
//!
//! # Get version from a specific Cargo.toml (standard cargo flag)
//! cargo version-info current --manifest-path ./path/to/Cargo.toml
//!
//! # Get JSON output
//! cargo version-info current --format json
//!
//! # Use in GitHub Actions
//! cargo version-info current --format github-actions
//! ```

use std::path::PathBuf;

use anyhow::{
    Context,
    Result,
};
use cargo_plugin_utils::common::find_package;
use clap::Parser;

/// Arguments for the `current` command.
#[derive(Parser, Debug)]
pub struct CurrentArgs {
    /// Path to the Cargo.toml manifest file (standard cargo flag).
    ///
    /// When running as a cargo subcommand, this is automatically handled.
    #[arg(long)]
    manifest_path: Option<PathBuf>,

    /// Output format for the version.
    ///
    /// - `version`: Print just the version number (e.g., "0.1.2")
    /// - `json`: Print JSON with version field
    /// - `github-actions`: Write to GITHUB_OUTPUT file in GitHub Actions format
    #[arg(long, default_value = "version")]
    format: String,

    /// Path to GitHub Actions output file.
    ///
    /// Only used when `--format github-actions` is specified.
    /// Defaults to the `GITHUB_OUTPUT` environment variable or stdout.
    #[arg(long, env = "GITHUB_OUTPUT")]
    github_output: Option<String>,
}

/// Get the current version from a Cargo.toml manifest file.
///
/// Extracts the version from the manifest, checking `[workspace.package]`
/// first (for workspace members), then falling back to `[package]`.
///
/// # Errors
///
/// Returns an error if:
/// - The manifest file cannot be read
/// - No version field is found in either `[workspace.package]` or `[package]`
/// - The output file cannot be written (for github-actions format)
///
/// # Examples
///
/// ```no_run
/// use cargo_version_info::commands::{
///     CurrentArgs,
///     current,
/// };
/// use clap::Parser;
/// # fn main() -> Result<(), Box<dyn std::error::Error>> {
/// // Parse from command line args
/// let args = CurrentArgs::parse_from(&["cargo", "version-info", "current"]);
/// current(args)?;
/// # Ok(())
/// # }
/// ```
///
/// # Example Output
///
/// With `--format version`:
/// ```text
/// 0.1.2
/// ```
///
/// With `--format json`:
/// ```json
/// {"version":"0.1.2"}
/// ```
///
/// With `--format github-actions` (writes to GITHUB_OUTPUT):
/// ```text
/// version=0.1.2
/// ```
pub fn current(args: CurrentArgs) -> Result<()> {
    let mut logger = cargo_plugin_utils::logger::Logger::new();

    logger.status("Reading", "package version");
    // Use find_package which automatically handles --manifest-path and workspace
    // logic
    let package = find_package(args.manifest_path.as_deref())?;
    let version = package.version.to_string();
    logger.finish();

    match args.format.as_str() {
        "version" => println!("{}", version),
        "json" => println!("{{\"version\":\"{}\"}}", version),
        "github-actions" => {
            let output_file = args.github_output.as_deref().unwrap_or("/dev/stdout");
            let output = format!("version={}\n", version);
            std::fs::write(output_file, output)
                .with_context(|| format!("Failed to write to {}", output_file))?;
        }
        _ => anyhow::bail!("Invalid format: {}", args.format),
    }

    Ok(())
}

#[cfg(test)]
mod tests {
    use tempfile::NamedTempFile;

    use super::*;

    fn create_temp_cargo_project(content: &str) -> tempfile::TempDir {
        let dir = tempfile::tempdir().unwrap();
        let manifest_path = dir.path().join("Cargo.toml");
        std::fs::write(&manifest_path, content).unwrap();

        // Create src directory with a minimal lib.rs for cargo metadata to work
        let src_dir = dir.path().join("src");
        std::fs::create_dir_all(&src_dir).unwrap();
        std::fs::write(src_dir.join("lib.rs"), "// Test library\n").unwrap();

        dir
    }

    #[test]
    fn test_current_workspace_version() {
        let _dir = tempfile::tempdir().unwrap();
        // Create workspace root Cargo.toml (no [package] section)
        std::fs::write(
            _dir.path().join("Cargo.toml"),
            r#"
[workspace.package]
version = "0.1.2"

[workspace]
members = ["member1"]
"#,
        )
        .unwrap();

        // Create member package
        let member_dir = _dir.path().join("member1");
        std::fs::create_dir_all(member_dir.join("src")).unwrap();
        std::fs::write(
            member_dir.join("Cargo.toml"),
            r#"
[package]
name = "member1"
version.workspace = true
"#,
        )
        .unwrap();
        std::fs::write(member_dir.join("src").join("lib.rs"), "// Test library\n").unwrap();

        // Test from the member package directory (where we'd normally run the command)
        let manifest_path = member_dir.join("Cargo.toml");
        let args = CurrentArgs {
            manifest_path: Some(manifest_path),
            format: "version".to_string(),
            github_output: None,
        };
        assert!(current(args).is_ok());
    }

    #[test]
    fn test_current_package_version() {
        let _dir = create_temp_cargo_project(
            r#"
[package]
name = "test"
version = "1.2.3"
"#,
        );
        let manifest_path = _dir.path().join("Cargo.toml");
        let args = CurrentArgs {
            manifest_path: Some(manifest_path.clone()),
            format: "version".to_string(),
            github_output: None,
        };
        let result = current(args);
        if let Err(e) = &result {
            eprintln!("Error in test_current_package_version: {}", e);
            eprintln!("Manifest path: {:?}", manifest_path);
        }
        assert!(result.is_ok());
    }

    #[test]
    fn test_current_json_format() {
        let _dir = create_temp_cargo_project(
            r#"
[package]
name = "test"
version = "0.5.0"
"#,
        );
        let manifest_path = _dir.path().join("Cargo.toml");
        let args = CurrentArgs {
            manifest_path: Some(manifest_path),
            format: "json".to_string(),
            github_output: None,
        };
        assert!(current(args).is_ok());
    }

    #[test]
    fn test_current_github_actions_format() {
        let _dir = create_temp_cargo_project(
            r#"
[package]
name = "test"
version = "2.0.0"
"#,
        );
        let manifest_path = _dir.path().join("Cargo.toml");
        let output_file = NamedTempFile::new().unwrap();
        let args = CurrentArgs {
            manifest_path: Some(manifest_path),
            format: "github-actions".to_string(),
            github_output: Some(output_file.path().to_string_lossy().to_string()),
        };
        assert!(current(args).is_ok());

        let content = std::fs::read_to_string(output_file.path()).unwrap();
        assert!(content.contains("version=2.0.0"));
    }

    #[test]
    fn test_current_invalid_format() {
        let _dir = create_temp_cargo_project(
            r#"
[package]
name = "test"
version = "1.0.0"
"#,
        );
        let manifest_path = _dir.path().join("Cargo.toml");
        let args = CurrentArgs {
            manifest_path: Some(manifest_path),
            format: "invalid".to_string(),
            github_output: None,
        };
        assert!(current(args).is_err());
    }

    #[test]
    fn test_current_file_not_found() {
        let args = CurrentArgs {
            manifest_path: Some("/nonexistent/Cargo.toml".into()),
            format: "version".to_string(),
            github_output: None,
        };
        assert!(current(args).is_err());
    }

    #[test]
    fn test_current_no_version() {
        // Cargo defaults to "0.0.0" when no version is specified, so this should
        // succeed
        let _dir = create_temp_cargo_project(
            r#"
[package]
name = "test"
"#,
        );
        let manifest_path = _dir.path().join("Cargo.toml");
        let args = CurrentArgs {
            manifest_path: Some(manifest_path),
            format: "version".to_string(),
            github_output: None,
        };
        // Cargo defaults to 0.0.0, so this should succeed
        let result = current(args);
        assert!(result.is_ok());
        // Verify it returns the default version
        // (We can't easily capture stdout in this test, but the function should
        // complete)
    }
}