version.rs

6.0 kB · rust · 199 lines

1use crate::disk::save;2use crate::model::{Manifest, Result};3use serde_json::Value;4use std::collections::{BTreeMap, BTreeSet};5use std::path::Path;6use std::process::Command;78pub const MANIFEST: &str = "pkgs/bridge/manifest.json";910const SOURCE: &str = "pkgs/mrlyrs/Cargo.toml";1112const STAMPS: &[(&str, &str)] = &[13    ("pkgs/mrlypy/Cargo.toml", "version = \""),14    ("pkgs/mrlyjs/package.json", "\"version\": \""),15];1617pub type Semver = (u64, u64, u64);1819pub struct Diff {20    pub added: usize,21    pub removed: Vec<String>,22    pub changed: Vec<String>,23}2425// STAMP2627pub fn read(root: &Path) -> Result<String> {28    let text = load(&root.join(SOURCE))?;29    let (start, end) =30        find(&text, "version = \"").ok_or_else(|| format!("no version in {SOURCE}"))?;31    Ok(text[start..end].to_string())32}3334pub fn write(manifest: &Manifest, root: &Path) -> Result<()> {35    for (file, key) in STAMPS {36        let path = root.join(file);37        let text = load(&path)?;38        let stamped = stamp(&text, key, &manifest.version).map_err(|e| format!("{file}: {e}"))?;39        save(&path, &stamped)?;40    }41    Ok(())42}4344pub fn stamp(text: &str, key: &str, version: &str) -> Result<String> {45    let (start, end) = find(text, key).ok_or_else(|| format!("no line starting {key}"))?;46    Ok(format!("{}{version}{}", &text[..start], &text[end..]))47}4849fn find(text: &str, key: &str) -> Option<(usize, usize)> {50    let mut at = 0;51    for line in text.split_inclusive('\n') {52        let indent = line.len() - line.trim_start().len();53        if line[indent..].starts_with(key) {54            let start = at + indent + key.len();55            return Some((start, start + text[start..].find('"')?));56        }57        at += line.len();58    }59    None60}6162fn load(path: &Path) -> Result<String> {63    std::fs::read_to_string(path).map_err(|e| format!("{}: {e}", path.display()))64}6566// BUMP6768pub fn bump(root: &Path) -> Result<()> {69    let new = json(&load(&root.join(MANIFEST))?)?;70    let now = new["version"]71        .as_str()72        .ok_or("the manifest has no version")?;73    let current = semver(now).ok_or_else(|| format!("{now} is not X.Y.Z"))?;74    let tags = git(root, &["tag", "-l", "v*"])?;75    let previous = tags76        .lines()77        .filter_map(|tag| Some((semver(tag.strip_prefix('v')?)?, tag)))78        .filter(|(version, _)| *version != current)79        .max();80    let Some((before, tag)) = previous else {81        println!("   no other v* tag; {now} is the first release");82        return Ok(());83    };84    let old = json(&git(root, &["show", &format!("{tag}:{MANIFEST}")])?)?;85    let diff = diff(&old, &new);86    for path in &diff.removed {87        println!("   removed {path}");88    }89    for path in &diff.changed {90        println!("   changed {path}");91    }92    println!(93        "   {tag} to {now}: {} added, {} removed, {} changed",94        diff.added,95        diff.removed.len(),96        diff.changed.len()97    );98    let breaking = !diff.removed.is_empty() || !diff.changed.is_empty();99    let floor = need(before, breaking);100    if current < floor {101        let what = if breaking {102            "a breaking change"103        } else {104            "a release"105        };106        return Err(format!(107            "{now} is too small a step over {tag}: {what} needs {} at least",108            show(floor)109        ));110    }111    Ok(())112}113114pub fn diff(old: &Value, new: &Value) -> Diff {115    let (old_all, old_ok) = entries(old);116    let (new_all, new_ok) = entries(new);117    let removed: BTreeSet<&String> = old_all118        .keys()119        .filter(|path| !new_all.contains_key(*path))120        .chain(old_ok.iter().filter(|path| !new_ok.contains(*path)))121        .collect();122    let changed = old_all123        .iter()124        .filter(|(path, sigs)| {125            !removed.contains(path) && new_all.get(*path).is_some_and(|new| !same(sigs, new))126        })127        .map(|(path, _)| path.clone())128        .collect();129    Diff {130        added: new_all131            .keys()132            .filter(|path| !old_all.contains_key(*path))133            .count(),134        removed: removed.into_iter().cloned().collect(),135        changed,136    }137}138139fn entries(manifest: &Value) -> (BTreeMap<String, Vec<Value>>, BTreeSet<String>) {140    let mut all: BTreeMap<String, Vec<Value>> = BTreeMap::new();141    let mut ok = BTreeSet::new();142    for f in manifest["functions"].as_array().into_iter().flatten() {143        let status = f["cross"]["status"].as_str().unwrap_or_default();144        let path = f["path"].as_str().unwrap_or_default().to_string();145        if status == "private" {146            continue;147        }148        if status == "ok" {149            ok.insert(path.clone());150        }151        let sig = ["self_kind", "params", "ret", "dims"].map(|key| f[key].clone());152        all.entry(path).or_default().push(Value::from(sig.to_vec()));153    }154    (all, ok)155}156157fn same(a: &[Value], b: &[Value]) -> bool {158    a.len() == b.len() && a.iter().all(|sig| b.contains(sig))159}160161pub fn need(before: Semver, breaking: bool) -> Semver {162    let (major, minor, patch) = before;163    match (breaking, major) {164        (false, _) => (major, minor, patch + 1),165        (true, 0) => (0, minor + 1, 0),166        (true, _) => (major + 1, 0, 0),167    }168}169170pub fn semver(text: &str) -> Option<Semver> {171    let mut parts = text.split('.').map(|part| part.parse::<u64>().ok());172    let version = (parts.next()??, parts.next()??, parts.next()??);173    parts.next().is_none().then_some(version)174}175176fn show((major, minor, patch): Semver) -> String {177    format!("{major}.{minor}.{patch}")178}179180fn json(text: &str) -> Result<Value> {181    serde_json::from_str(text).map_err(|e| e.to_string())182}183184fn git(root: &Path, args: &[&str]) -> Result<String> {185    let out = Command::new("git")186        .arg("-C")187        .arg(root)188        .args(args)189        .output()190        .map_err(|e| format!("git: {e}"))?;191    if !out.status.success() {192        return Err(format!(193            "git {}: {}",194            args.join(" "),195            String::from_utf8_lossy(&out.stderr).trim()196        ));197    }198    String::from_utf8(out.stdout).map_err(|e| e.to_string())199}