cargo-ratchet 0.1.0

Prevent resolved versions in Cargo.lock from regressing below a committed, self-advancing baseline
Documentation
use std::path::PathBuf;
use std::process::ExitCode;

use anyhow::Result;
use cargo_ratchet::{RatchetPaths, check_and_advance, check_fail_on_change};
use clap::Parser;

/// Cargo dispatches `cargo ratchet ...` as `cargo-ratchet ratchet ...`, so
/// the CLI is modeled as a `cargo` wrapper with a single subcommand.
#[derive(Parser)]
#[command(name = "cargo", bin_name = "cargo")]
enum Cargo {
    Ratchet(RatchetArgs),
}

/// Fail when resolved versions in Cargo.lock regress below a committed
/// baseline; advance the baseline when the check passes.
#[derive(clap::Args)]
#[command(version, about)]
struct RatchetArgs {
    /// Never write; fail unless the baseline is byte-identical to the
    /// lockfile (run `cargo ratchet` locally and commit to fix)
    #[arg(long)]
    fail_on_change: bool,

    /// Report violations as JSON on stdout instead of text on stderr
    #[arg(long)]
    json: bool,

    /// Path to Cargo.toml
    #[arg(long, value_name = "PATH")]
    manifest_path: Option<PathBuf>,

    /// Path to the lockfile [default: <workspace root>/Cargo.lock]
    #[arg(long, value_name = "PATH")]
    lockfile: Option<PathBuf>,

    /// Path to the baseline file [default: .<lockfile name>.ratchet next to
    /// the lockfile]
    #[arg(long, value_name = "PATH")]
    ratchet_file: Option<PathBuf>,
}

fn main() -> Result<ExitCode> {
    let Cargo::Ratchet(args) = Cargo::parse();

    let paths = RatchetPaths::discover(
        args.manifest_path.as_deref(),
        args.lockfile,
        args.ratchet_file,
    )?;

    if args.fail_on_change {
        if check_fail_on_change(&paths)? {
            return Ok(ExitCode::SUCCESS);
        }
        eprintln!(
            "error: {} does not match {}; run `cargo ratchet` locally and commit the result",
            paths.ratchet_file.display(),
            paths.lockfile.display()
        );
        return Ok(ExitCode::FAILURE);
    }

    let violations = check_and_advance(&paths)?;
    if violations.is_empty() {
        return Ok(ExitCode::SUCCESS);
    }
    if args.json {
        println!("{}", serde_json::to_string_pretty(&violations)?);
    } else {
        for violation in &violations {
            eprintln!("error: {violation}");
        }
    }
    Ok(ExitCode::FAILURE)
}