From 4248c7054d842757c1ce0a2b7ef6a55f17723791 Mon Sep 17 00:00:00 2001 From: Matt Yan Date: Thu, 26 Mar 2026 23:21:43 +0900 Subject: [PATCH] feat(dev-tool): what if i bump --- .gitignore | 1 + CHANGELOG.md | 149 ++++---- Cargo.toml | 1 + tools/what-if-i-bump/Cargo.lock | 141 +++++++ tools/what-if-i-bump/Cargo.toml | 7 + tools/what-if-i-bump/src/main.rs | 607 +++++++++++++++++++++++++++++++ 6 files changed, 829 insertions(+), 77 deletions(-) create mode 100644 tools/what-if-i-bump/Cargo.lock create mode 100644 tools/what-if-i-bump/Cargo.toml create mode 100644 tools/what-if-i-bump/src/main.rs diff --git a/.gitignore b/.gitignore index 5c8463df..44df4b80 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,5 @@ /target +tools/what-if-i-bump/target **/*.rs.bk examples/*/dist diff --git a/CHANGELOG.md b/CHANGELOG.md index 6f0424c6..63b27bf0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,4 +1,10 @@ -## `console` +## `gloo` + +### Version 0.12.0 + +- Bump all sub-crate versions, update MSRV to 1.82 + +## `gloo-console` ### Version 0.4.0 @@ -10,56 +16,56 @@ - Introduces the `FromQuery` and `ToQuery` traits to allow for customizing how query strings are encoded and decoded in `gloo_history`. (#364) -### Version "0.2.3" +### Version 0.2.3 - Release new gloo versions -### Version "0.2.2" +### Version 0.2.2 - feat(gloo-utils): Lift serde-serialization from wasm-bindgen (#242) - fix: Break dependency cycle by not using serde-serialize (#239) -### Version "0.2.1" +### Version 0.2.1 - Fix `utils` crate and `history` docs. (#189) - Hash-based History type & Unified Location. (#177) - Fixes `console_dbg!` and `console!` expression output. Bold src info. (#174) -### Version "0.2.0" +### Version 0.2.0 - Add console_dbg macro (#170) -### Version "0.1.0" +### Version 0.1.0 - Add an `dbg!` equivalent to `gloo-console` (#158) - Fix dir, dirxml macros in gloo-console (#154) - Finishing touches -## `dialogs` +## `gloo-dialogs` ### Version 0.3.0 - Update MSRV to 1.82 (#505) by @martinfrances107 -### Version "0.2.0" +### Version 0.2.0 - Migrate to Edition 2021 and Apply MSRV in Cargo.toml (#360) -### Version "0.1.0" +### Version 0.1.0 -## `events` +## `gloo-events` ### Version 0.3.0 - Update MSRV to 1.82 (#505) by @martinfrances107 - Bump `web-sys` minimum to 0.3.91 (#536) by @Madoshakalaka -### Version "0.2.0" +### Version 0.2.0 - Migrate to Edition 2021 and Apply MSRV in Cargo.toml (#360) - Add traits `PartialEq` and `Eq` to `EventListenerOptions`, and `EventListenerPhase` (#363) -### Version "0.1.1" +### Version 0.1.1 - Add implementation for rfc-1 (files) - Update readmes @@ -69,7 +75,7 @@ - Migrate to futures 0.3 - Bump events version -### Version "0.1.0" +### Version 0.1.0 - Updating the links in the events crate - Adding in crate metadata for events @@ -92,7 +98,7 @@ - Renaming A parameter to S - Fixing all the issues with gloo-events -## `file` +## `gloo-file` ### Version 0.4.0 @@ -100,21 +106,21 @@ - Update MSRV to 1.82 (#505) by @martinfrances107 - Bump `web-sys` minimum to 0.3.91 (#536) by @Madoshakalaka -### Version "0.3.0" +### Version 0.3.0 - Migrate to Edition 2021 and Apply MSRV in Cargo.toml (#360) -### Version "0.2.2" +### Version 0.2.2 - [rfc] Implement an ObjectUrl wrapper (#231) -### Version "0.2.0" +### Version 0.2.0 - impl Clone and PartialEq (#184) - Hash-based History type & Unified Location. (#177) - Prepare for 0.4 release (#156) -### Version "0.1.0" +### Version 0.1.0 - Remove the unnecessary copy in `Blob::new` (#152) - Prepare v0.3.0 release (#148) @@ -124,7 +130,7 @@ - Adding README for gloo-file - Fixing Cargo.toml for gloo-file -## `history` +## `gloo-history` ### Version 0.3.0 @@ -151,33 +157,33 @@ - Use `thread_local!`'s lazy initialization instead of `RefCell>` (#358) - Fix required feature set of serde dependency (#357) -### Version "0.1.3" +### Version 0.1.3 - Fix some typos (#313) - Update serde-wasm-bindgen requirement from 0.4.5 to 0.5.0 (#320) - Fix clippy for Rust 1.67 (#302) -### Version "0.1.2" +### Version 0.1.2 - (history): Drop states borrow before callback invocation (#285) - Update serde-wasm-bindgen requirement from 0.3.1 to 0.4.5 (#297) - Fix clippy. (#287) -### Version "0.1.1" +### Version 0.1.1 - Fix history tests (#252) - Add query() method (#215) - Fix failing history tests (#219) - Fix links in gloo-history README (#210) -### Version "0.1.0" +### Version 0.1.0 - Fix `utils` crate and `history` docs. (#189) - 0.5.0 - Memory-based History (#178) - Hash-based History type & Unified Location. (#177) -## `net` +## `gloo-net` ### Version 0.7.0 @@ -191,42 +197,42 @@ - Update MSRV to 1.82 (#505) by @martinfrances107 - Bump `pin-project` to 1.1 and `http` to 1.4 (#536) by @Madoshakalaka -### Version "0.6.0" +### Version 0.6.0 - Update http crate to 1.0 (#407) -### Version "0.5.0" +### Version 0.5.0 - Implement `futures_io::AsyncWrite` and `futures_io::AsyncRead` on `WebSocket`. This feature is behind a new feature flag `io-util` that is disabled by default. - Add `TryFrom` (#365) - Add WASI support for gloo-history. (#405) -### Version "0.4.0" +### Version 0.4.0 - Migrate to Edition 2021 and Apply MSRV in Cargo.toml (#360) -### Version "0.3.1" +### Version 0.3.1 - export RequestBuilder and ResponseBuilder as public -### Version "0.3.0" +### Version 0.3.0 - Seanaye/feat/serverside http (#312) -### Version "0.2.6" +### Version 0.2.6 - Add `PartialEq, Eq, PartialOrd, Ord, Hash` for eventsource `State` (#336) - Seanaye/feat/serverside http (#312) - Fix clippy for Rust 1.67 (#302) -### Version "0.2.5" +### Version 0.2.5 - Fix clippy. (#287) - Prevent send from hanging if connection fails. (#280) -### Version "0.2.4" +### Version 0.2.4 - fix(ws): calling close event with destroyed close callback (#264) - fix: cyclic dependency for gloo-net websocket feature (#260) @@ -237,99 +243,99 @@ - Provides an EventSource implementation (#246) - Release new gloo versions -### Version "0.2.3" +### Version 0.2.3 - feat(gloo-utils): Lift serde-serialization from wasm-bindgen (#242) - Fix feature soundness issues with gloo-net (#243) - fix: Break dependency cycle by not using serde-serialize (#239) - gloo-net v0.2.3 -### Version "0.2.2" +### Version 0.2.2 - Add missing feature flags to gloo-net (#230) - gloo-net v0.2.2 -### Version "0.2.1" +### Version 0.2.1 - Feature soundness of gloo-http (#228) - Release v0.8.0 -### Version "0.2.0" +### Version 0.2.0 - Added support for specifying Websocket Protocols (#222) - Add query() method (#215) - Move UncheckedIter to gloo-utils (#217) - docs: revise docs for gloo_net::http::Request.method (#212) -### Version "0.1.0" +### Version 0.1.0 - add `json()` impl to `Request` (#204) - Improve the Fetch API (#188) -## `render` +## `gloo-render` ### Version 0.3.0 - Update MSRV to 1.82 (#505) by @martinfrances107 -### Version "0.2.0" +### Version 0.2.0 - Migrate to Edition 2021 and Apply MSRV in Cargo.toml (#360) -### Version "0.1.0" +### Version 0.1.0 -## `storage` +## `gloo-storage` ### Version 0.4.0 - Update MSRV to 1.82 (#505) by @martinfrances107 -### Version "0.3.0" +### Version 0.3.0 - Migrate to Edition 2021 and Apply MSRV in Cargo.toml (#360) -### Version "0.2.1" +### Version 0.2.1 - fix: Break dependency cycle by not using serde-serialize (#239) -### Version "0.2.0" +### Version 0.2.0 - Fix up gloo-storage for release - Prepare for 0.4 release (#156) -### Version "0.1.0" +### Version 0.1.0 - Utility crate for common `web_sys`/`js_sys` features (#155) -## `timers` +## `gloo-timers` ### Version 0.4.0 - Update MSRV to 1.82 (#505) by @martinfrances107 -### Version "0.3.0" +### Version 0.3.0 - Migrate to Edition 2021 and Apply MSRV in Cargo.toml (#360) -### Version "0.2.5" +### Version 0.2.5 - fix: `clearTimeout` illegal invocation with bundler (#187) (#283) -### Version "0.2.4" +### Version 0.2.4 - gloo_timers, ambiguous verbage (#255) -### Version "0.2.3" +### Version 0.2.3 - New patch versions -### Version "0.2.2" +### Version 0.2.2 - Remove `web-sys` dependency (#186) - Add node.js support for timers (#185) - 0.5.0 -### Version "0.2.1" +### Version 0.2.1 - Hash-based History type & Unified Location. (#177) - Add BrowserHistory and BrowserLocation (#171) @@ -338,7 +344,7 @@ - Make docs.rs include futures functionality (#116) - gloo-timers 0.2.1 -### Version "0.2.0" +### Version 0.2.0 - Add implementation for rfc-1 (files) - Change implementation of getting new global. @@ -347,7 +353,7 @@ - Fix timers to work in workers too. - Preparing for release -### Version "0.1.0" +### Version 0.1.0 - Changing to use web-sys; this fixes a Webpack error - Fix missing wasm_bindgen import. @@ -375,52 +381,52 @@ - Split up callback and future/stream APIs. - timers: Fix author line in Cargo.toml -## `utils` +## `gloo-utils` ### Version 0.3.0 - Fix `JsError::try_from` panicking on non-string-coercible `JsValue` inputs (#488) by @ranile - Update MSRV to 1.82, migrate dependencies to workspace versions (#505) by @martinfrances107 -### Version "0.2.0" +### Version 0.2.0 - Migrate to Edition 2021 and Apply MSRV in Cargo.toml (#360) -### Version "0.1.6" +### Version 0.1.6 - Update json.rs fix typo (#338) - docs: correct format examples Fixes #276 (#278) -### Version "0.1.5" +### Version 0.1.5 - refactor: typo fix (#262) - Release new gloo versions -### Version "0.1.4" +### Version 0.1.4 - feat(gloo-utils): Lift serde-serialization from wasm-bindgen (#242) - Release v0.8.0 -### Version "0.1.3" +### Version 0.1.3 - Implement std Error trait for JsError (#225) - Move UncheckedIter to gloo-utils (#217) -### Version "0.1.2" +### Version 0.1.2 - Fix `utils` crate and `history` docs. (#189) - Gloo v0.6.0 -### Version "0.1.1" +### Version 0.1.1 - Html head access (#179) - 0.4.2 -### Version "0.1.0" +### Version 0.1.0 - utils: Add body() and document_element() getters (#161) -## `worker` +## `gloo-worker` ### Version 0.6.0 @@ -445,22 +451,11 @@ - Function Worker (#267) -### Version "0.2.0" +### Version 0.2.0 - Release v0.8.0 -### Version "0.1.1" +### Version 0.1.1 - Worker v2 (#200) - Remove the private worker refcell (#223) - -## `worker-macros` - -### Version 0.2.0 - -- Update MSRV to 1.82 via workspace inheritance (#505) by @martinfrances107 -- Bump `proc-macro-crate` to 3.x, pin for MSRV 1.82 compatibility (#505, #536) by @martinfrances107, @Madoshakalaka - -### Version 0.1.0 - -- Initial release with `#[oneshot]` and `#[reactor]` procedural macros (#267) diff --git a/Cargo.toml b/Cargo.toml index 007e1eed..deae1715 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -70,6 +70,7 @@ worker = ["gloo-worker"] net = ["gloo-net"] [workspace] +exclude = ["tools/what-if-i-bump"] members = [ "crates/timers", "crates/events", diff --git a/tools/what-if-i-bump/Cargo.lock b/tools/what-if-i-bump/Cargo.lock new file mode 100644 index 00000000..3338af59 --- /dev/null +++ b/tools/what-if-i-bump/Cargo.lock @@ -0,0 +1,141 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 4 + +[[package]] +name = "equivalent" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" + +[[package]] +name = "hashbrown" +version = "0.16.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "841d1cc9bed7f9236f321df977030373f4a4163ae1a7dbfe1a51a2c1a51d9100" + +[[package]] +name = "indexmap" +version = "2.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7714e70437a7dc3ac8eb7e6f8df75fd8eb422675fc7678aff7364301092b1017" +dependencies = [ + "equivalent", + "hashbrown", +] + +[[package]] +name = "proc-macro2" +version = "1.0.106" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8fd00f0bb2e90d81d1044c2b32617f68fcb9fa3bb7640c23e9c748e53fb30934" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "quote" +version = "1.0.45" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41f2619966050689382d2b44f664f4bc593e129785a36d6ee376ddf37259b924" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "serde_core" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "serde_spanned" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "876ac351060d4f882bb1032b6369eb0aef79ad9df1ea8bc404874d8cc3d0cd98" +dependencies = [ + "serde_core", +] + +[[package]] +name = "syn" +version = "2.0.117" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e665b8803e7b1d2a727f4023456bbbbe74da67099c585258af0ad9c5013b9b99" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "toml" +version = "1.1.0+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8195ca05e4eb728f4ba94f3e3291661320af739c4e43779cbdfae82ab239fcc" +dependencies = [ + "indexmap", + "serde_core", + "serde_spanned", + "toml_datetime", + "toml_parser", + "toml_writer", + "winnow", +] + +[[package]] +name = "toml_datetime" +version = "1.1.0+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "97251a7c317e03ad83774a8752a7e81fb6067740609f75ea2b585b569a59198f" +dependencies = [ + "serde_core", +] + +[[package]] +name = "toml_parser" +version = "1.1.0+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2334f11ee363607eb04df9b8fc8a13ca1715a72ba8662a26ac285c98aabb4011" +dependencies = [ + "winnow", +] + +[[package]] +name = "toml_writer" +version = "1.1.0+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d282ade6016312faf3e41e57ebbba0c073e4056dab1232ab1cb624199648f8ed" + +[[package]] +name = "unicode-ident" +version = "1.0.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75" + +[[package]] +name = "what-if-i-bump" +version = "0.1.0" +dependencies = [ + "toml", +] + +[[package]] +name = "winnow" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a90e88e4667264a994d34e6d1ab2d26d398dcdca8b7f52bec8668957517fc7d8" diff --git a/tools/what-if-i-bump/Cargo.toml b/tools/what-if-i-bump/Cargo.toml new file mode 100644 index 00000000..bc6db4e5 --- /dev/null +++ b/tools/what-if-i-bump/Cargo.toml @@ -0,0 +1,7 @@ +[package] +name = "what-if-i-bump" +version = "0.1.0" +edition = "2021" + +[dependencies] +toml = "1.1" diff --git a/tools/what-if-i-bump/src/main.rs b/tools/what-if-i-bump/src/main.rs new file mode 100644 index 00000000..93b50aaf --- /dev/null +++ b/tools/what-if-i-bump/src/main.rs @@ -0,0 +1,607 @@ +use std::collections::BTreeMap; +use std::env; +use std::fmt; +use std::fs; +use std::path::{Path, PathBuf}; +use std::process; + +fn main() { + let args: Vec = env::args().collect(); + + let workspace_root = find_workspace_root().unwrap_or_else(|| { + eprintln!("error: run this tool from within the gloo workspace"); + process::exit(1); + }); + + let mut crates = discover_crates(&workspace_root); + apply_changelog_status(&workspace_root.join("CHANGELOG.md"), &mut crates); + + if args.len() < 3 { + print_status(&crates); + if args.len() == 1 { + eprintln!(); + eprintln!("Usage: what-if-i-bump "); + eprintln!("Example: what-if-i-bump net minor"); + } + return; + } + + let target_short = &args[1]; + let bump_arg = &args[2]; + + let target = resolve_crate(target_short, &crates); + if target == "gloo" { + eprintln!("error: cannot directly bump the root crate; bump a sub-crate instead"); + process::exit(1); + } + + let bump = match bump_arg.as_str() { + "patch" => BumpKind::Patch, + "minor" => { + if crates[&target].cargo_version.major == 0 { + BumpKind::ZeroMinor + } else { + BumpKind::NonZeroMinor + } + } + "major" => BumpKind::Major, + other => { + eprintln!( + "error: unknown bump type '{}'. Use: patch, minor, major", + other + ); + process::exit(1); + } + }; + + let reverse_deps = build_reverse_deps(&crates); + let required = compute_cascade(&target, bump, &crates, &reverse_deps); + + print_plan(&target, &crates, &required); +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +struct Version { + major: u32, + minor: u32, + patch: u32, +} + +impl Version { + fn parse(s: &str) -> Option { + let s = s.trim().trim_matches('"'); + let mut parts = s.splitn(3, '.'); + Some(Self { + major: parts.next()?.parse().ok()?, + minor: parts.next()?.parse().ok()?, + patch: parts.next()?.parse().ok()?, + }) + } + + fn bumped(self, kind: BumpKind) -> Self { + match kind { + BumpKind::Level => self, + BumpKind::Patch => Self { + patch: self.patch + 1, + ..self + }, + BumpKind::ZeroMinor => Self { + major: 0, + minor: self.minor + 1, + patch: 0, + }, + BumpKind::NonZeroMinor => Self { + minor: self.minor + 1, + patch: 0, + ..self + }, + BumpKind::Major => Self { + major: self.major + 1, + minor: 0, + patch: 0, + }, + } + } +} + +impl Ord for Version { + fn cmp(&self, other: &Self) -> std::cmp::Ordering { + self.major + .cmp(&other.major) + .then(self.minor.cmp(&other.minor)) + .then(self.patch.cmp(&other.patch)) + } +} + +impl PartialOrd for Version { + fn partial_cmp(&self, other: &Self) -> Option { + Some(self.cmp(other)) + } +} + +impl fmt::Display for Version { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "{}.{}.{}", self.major, self.minor, self.patch) + } +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)] +enum BumpKind { + Level, + Patch, + NonZeroMinor, + ZeroMinor, + Major, +} + +impl fmt::Display for BumpKind { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.write_str(match self { + Self::Level => "level", + Self::Patch => "patch-bumping", + Self::NonZeroMinor => "non-zero-minor-bumping", + Self::ZeroMinor => "zero-minor-bumping", + Self::Major => "major-bumping", + }) + } +} + +fn bump_kind_between(from: &Version, to: &Version) -> BumpKind { + if to <= from { + return BumpKind::Level; + } + if to.major > from.major { + return BumpKind::Major; + } + if to.minor > from.minor { + return if from.major == 0 { + BumpKind::ZeroMinor + } else { + BumpKind::NonZeroMinor + }; + } + BumpKind::Patch +} + +#[derive(Debug, Clone)] +struct CrateInfo { + name: String, + short_name: String, + cargo_version: Version, + changelog_version: Option, + status: BumpKind, + internal_deps: Vec, + is_macro_crate: bool, + changelog_section: String, +} + +fn find_workspace_root() -> Option { + let mut dir = env::current_dir().ok()?; + loop { + let cargo = dir.join("Cargo.toml"); + if cargo.exists() { + let content = fs::read_to_string(&cargo).ok()?; + if content.contains("[workspace]") { + return Some(dir); + } + } + if !dir.pop() { + return None; + } + } +} + +fn discover_crates(root: &Path) -> BTreeMap { + let mut crates = BTreeMap::new(); + + let root_toml: toml::Value = fs::read_to_string(root.join("Cargo.toml")) + .expect("read root Cargo.toml") + .parse() + .expect("parse root Cargo.toml"); + + let root_ver = Version::parse(root_toml["package"]["version"].as_str().unwrap()).unwrap(); + let root_deps = extract_gloo_deps(&root_toml); + + crates.insert( + "gloo".into(), + CrateInfo { + name: "gloo".into(), + short_name: "gloo".into(), + cargo_version: root_ver, + changelog_version: None, + status: BumpKind::Level, + internal_deps: root_deps, + is_macro_crate: false, + changelog_section: "gloo".into(), + }, + ); + + let crates_dir = root.join("crates"); + let mut entries: Vec<_> = fs::read_dir(&crates_dir) + .expect("read crates/") + .filter_map(|e| e.ok()) + .collect(); + entries.sort_by_key(|e| e.file_name()); + + for entry in entries { + let path = entry.path(); + let cargo_path = path.join("Cargo.toml"); + if !cargo_path.exists() { + continue; + } + + let toml_val: toml::Value = fs::read_to_string(&cargo_path) + .unwrap_or_else(|_| panic!("read {}", cargo_path.display())) + .parse() + .unwrap_or_else(|_| panic!("parse {}", cargo_path.display())); + + let name = toml_val["package"]["name"].as_str().unwrap().to_string(); + let version = Version::parse(toml_val["package"]["version"].as_str().unwrap()).unwrap(); + let short = name.strip_prefix("gloo-").unwrap_or(&name).to_string(); + let deps = extract_gloo_deps(&toml_val); + + let is_macro = short.ends_with("-macros"); + let changelog_section = if is_macro { + format!("gloo-{}", short.strip_suffix("-macros").unwrap()) + } else { + name.clone() + }; + + crates.insert( + name.clone(), + CrateInfo { + name, + short_name: short, + cargo_version: version, + changelog_version: None, + status: BumpKind::Level, + internal_deps: deps, + is_macro_crate: is_macro, + changelog_section, + }, + ); + } + + crates +} + +fn extract_gloo_deps(toml_val: &toml::Value) -> Vec { + let mut deps = Vec::new(); + if let Some(table) = toml_val.get("dependencies").and_then(|d| d.as_table()) { + for key in table.keys() { + if key.starts_with("gloo-") { + deps.push(key.clone()); + } + } + } + deps.sort(); + deps +} + +fn apply_changelog_status(path: &Path, crates: &mut BTreeMap) { + let content = fs::read_to_string(path).expect("read CHANGELOG.md"); + let mut current_section: Option = None; + let mut found_version_for_section = false; + + for line in content.lines() { + let t = line.trim(); + + if let Some(rest) = t.strip_prefix("## `") { + if let Some(name) = rest.strip_suffix('`') { + current_section = Some(name.to_string()); + found_version_for_section = false; + continue; + } + } + + if !found_version_for_section { + if let Some(ref section) = current_section { + if let Some(ver_str) = t.strip_prefix("### Version ") { + if let Some(ver) = Version::parse(ver_str) { + for info in crates.values_mut() { + if info.changelog_section == *section && !info.is_macro_crate { + info.changelog_version = Some(ver); + info.status = bump_kind_between(&info.cargo_version, &ver); + } + } + found_version_for_section = true; + } + } + } + } + } +} + +fn resolve_crate(input: &str, crates: &BTreeMap) -> String { + if crates.contains_key(input) { + return input.to_string(); + } + let full = format!("gloo-{}", input); + if crates.contains_key(&full) { + return full; + } + for info in crates.values() { + if info.short_name == input { + return info.name.clone(); + } + } + eprintln!("error: unknown crate '{}'", input); + eprintln!("available crates:"); + for info in crates.values() { + if info.name != "gloo" { + eprintln!(" {} ({})", info.short_name, info.name); + } + } + process::exit(1); +} + +fn build_reverse_deps(crates: &BTreeMap) -> BTreeMap> { + let mut rev: BTreeMap> = BTreeMap::new(); + for info in crates.values() { + for dep in &info.internal_deps { + rev.entry(dep.clone()).or_default().push(info.name.clone()); + } + } + rev +} + +fn compute_cascade( + target: &str, + bump: BumpKind, + crates: &BTreeMap, + reverse_deps: &BTreeMap>, +) -> BTreeMap { + let mut required: BTreeMap = BTreeMap::new(); + required.insert(target.to_string(), bump); + + loop { + let mut changed = false; + let snapshot: Vec<_> = required.iter().map(|(k, &v)| (k.clone(), v)).collect(); + + for (name, child_bump) in snapshot { + if let Some(parents) = reverse_deps.get(&name) { + for parent in parents { + if !crates.contains_key(parent) { + continue; + } + let cascade = child_bump; + let entry = required.entry(parent.clone()).or_insert(BumpKind::Level); + if cascade > *entry { + *entry = cascade; + changed = true; + } + } + } + } + + if !changed { + break; + } + } + + required +} + +fn print_status(crates: &BTreeMap) { + println!("Current crate status:"); + println!(); + + let max_name = crates.values().map(|c| c.name.len()).max().unwrap_or(0); + + for info in crates.values() { + if info.is_macro_crate { + println!( + " {: "level".to_string(), + _ => { + let cv = info.changelog_version.unwrap(); + format!("{} (-> {})", info.status, cv) + } + }; + println!( + " {:, + required: &BTreeMap, +) { + let mut new_versions: BTreeMap = BTreeMap::new(); + for (name, &needed) in required { + let info = &crates[name]; + let effective = std::cmp::max(needed, info.status); + new_versions.insert(name.clone(), info.cargo_version.bumped(effective)); + } + + struct Action { + name: String, + cargo_ver: Version, + current_status: BumpKind, + effective_bump: BumpKind, + new_version: Version, + needs_change: bool, + is_macro_crate: bool, + changelog_section: String, + dep_bumps: Vec<(String, Version)>, + } + + let mut actions: Vec = Vec::new(); + + for (name, &needed) in required { + let info = &crates[name]; + let effective = std::cmp::max(needed, info.status); + let new_ver = new_versions[name]; + let needs_change = effective > info.status; + + let mut dep_bumps = Vec::new(); + for dep in &info.internal_deps { + if let Some(dep_ver) = new_versions.get(dep) { + let dep_info = &crates[dep]; + dep_bumps.push((dep_info.name.clone(), *dep_ver)); + } + } + + actions.push(Action { + name: name.clone(), + cargo_ver: info.cargo_version, + current_status: info.status, + effective_bump: effective, + new_version: new_ver, + needs_change, + is_macro_crate: info.is_macro_crate, + changelog_section: info.changelog_section.clone(), + dep_bumps, + }); + } + + actions.sort_by(|a, b| { + let a_is_root = a.name == "gloo"; + let b_is_root = b.name == "gloo"; + let a_is_target = a.name == target; + let b_is_target = b.name == target; + // target first, then non-root alphabetically, root last + if a_is_target != b_is_target { + return b_is_target.cmp(&a_is_target); + } + if a_is_root != b_is_root { + return a_is_root.cmp(&b_is_root); + } + a.name.cmp(&b.name) + }); + + // Print summary table + println!("Bump cascade:"); + println!(); + + let max_name = actions.iter().map(|a| a.name.len()).max().unwrap_or(0); + + for a in &actions { + let current = if a.current_status == BumpKind::Level { + format!("{}", a.cargo_ver) + } else { + let cl = crates[&a.name].changelog_version.unwrap(); + format!("{} (-> {})", a.cargo_ver, cl) + }; + + let arrow_and_target = if a.needs_change { + format!("=> {} ({})", a.new_version, a.effective_bump) + } else { + " (already satisfied)".to_string() + }; + + let reason = if a.name == target { + "(target)".to_string() + } else if a.dep_bumps.is_empty() { + String::new() + } else { + let deps: Vec<_> = a.dep_bumps.iter().map(|(n, _)| n.as_str()).collect(); + format!("[depends on {}]", deps.join(", ")) + }; + + println!( + " {: = actions.iter().filter(|a| a.needs_change).collect(); + + if edits.is_empty() { + println!(); + println!("All required CHANGELOG sections already exist. Add your changes there."); + return; + } + + println!(); + println!("CHANGELOG.md edits:"); + println!(); + + // Group by changelog section to merge macro crate bumps into parent section + struct SectionEdit { + section: String, + new_version: Version, + is_new_section: bool, + old_version: Option, + dep_lines: Vec, + } + + let mut section_edits: BTreeMap = BTreeMap::new(); + + for a in &edits { + let section = &a.changelog_section; + let entry = section_edits + .entry(section.clone()) + .or_insert_with(|| SectionEdit { + section: section.clone(), + new_version: a.new_version, + is_new_section: a.current_status == BumpKind::Level, + old_version: crates[&a.name].changelog_version, + dep_lines: Vec::new(), + }); + + if a.is_macro_crate { + entry + .dep_lines + .push(format!("- Bump `{}` to {}", a.name, a.new_version)); + } else { + entry.new_version = a.new_version; + entry.is_new_section = a.current_status == BumpKind::Level; + entry.old_version = crates[&a.name].changelog_version; + for (dep_name, dep_ver) in &a.dep_bumps { + if !crates[dep_name].is_macro_crate { + entry + .dep_lines + .push(format!("- Bump `{}` to {}", dep_name, dep_ver)); + } + } + } + } + + for edit in section_edits.values() { + if edit.is_new_section { + println!(" ## `{}` -- add at top:", edit.section); + println!(); + println!(" ### Version {}", edit.new_version); + if !edit.dep_lines.is_empty() { + println!(); + for line in &edit.dep_lines { + println!(" {}", line); + } + } + } else { + let old = edit.old_version.unwrap(); + println!( + " ## `{}` -- rename top version: {} => {}", + edit.section, old, edit.new_version + ); + if !edit.dep_lines.is_empty() { + println!(); + println!(" Also add:"); + for line in &edit.dep_lines { + println!(" {}", line); + } + } + } + println!(); + } +}