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}