cargo-ratchet 0.1.0

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

use anyhow::{Context, Result};
use cargo_lock::Lockfile;

use crate::{RatchetPaths, Violation, compute_violations};

/// Check the lockfile against the ratchet baseline and, when the check passes
/// (an empty return), copy the lockfile over the baseline so the floor
/// ratchets up. A missing baseline is not a violation: the first run
/// bootstraps it.
///
/// Checking and advancing are deliberately one operation: always advancing on
/// a passing check is what produces the ratchet effect, and requiring a
/// passing check to advance prevents baseline updates from slipping in
/// unchecked.
pub fn check_and_advance(paths: &RatchetPaths) -> Result<Vec<Violation>> {
    let current = read_lockfile(&paths.lockfile)?;

    if paths.ratchet_file.exists() {
        let baseline = read_lockfile(&paths.ratchet_file)?;
        let violations = compute_violations(&baseline, &current);
        if !violations.is_empty() {
            return Ok(violations);
        }
    }

    // Copy via a sibling temp file and rename, so an interrupted run can't
    // leave a truncated baseline.
    let temp_file = append_extension(&paths.ratchet_file, ".tmp");
    fs::copy(&paths.lockfile, &temp_file).with_context(|| {
        format!(
            "failed to copy {} to {}",
            paths.lockfile.display(),
            temp_file.display()
        )
    })?;
    fs::rename(&temp_file, &paths.ratchet_file).with_context(|| {
        format!(
            "failed to rename {} to {}",
            temp_file.display(),
            paths.ratchet_file.display()
        )
    })?;
    Ok(Vec::new())
}

fn read_lockfile(path: &Path) -> Result<Lockfile> {
    let contents =
        fs::read_to_string(path).with_context(|| format!("failed to read {}", path.display()))?;
    contents
        .parse()
        .with_context(|| format!("failed to parse {}", path.display()))
}

fn append_extension(path: &Path, extension: &str) -> PathBuf {
    let mut with_extension = path.to_path_buf().into_os_string();
    with_extension.push(extension);
    PathBuf::from(with_extension)
}