I Optimized My Bump Version Tool and Made It 1,000,000x Faster Than Its Python Counterparts.
Hello again, fellow Rustaceans, Pythonistas in denial, and the three people on earth who genuinely enjoy reading version bumper release notes π! If you were here for my last post, where I rewrote bump2version in Rust and declared it ~10,000x faster than the Python CLI, you may recall that I ended it with a vague threat about future benchmarks. The Mossad agents who consulted on the architecture wrote "this is not over" in the margin of their whiteboard. I ignored it. I should have listened. Because in the weeks since that post, I went back in. Deep in. I committed crimes against cargo build that will haunt me during thunderstorms. I --release'd things that should not be --release'd. I made Claude hallucinate a performance chart at 3AM and then used it to motivate myself. The result: bump2version 0.2.1. Now 1,000,000Γ faster than its Python counterparts. Yes, I am counting subprocess overhead. The Soviet material resurfaced. It always does. Let's get into it. π€ Wait, We Already Did This. Why Are We Here Again? Because 0.2.0 shipped, and I immediately opened my Gmail inbox. The audacity of the open-source community. The audacity, bro! I build something 10,000x faster and within days there are requests. "Can it watch files?", "Can it detect which manifests I'm using?", "Can I run it in the browser?", "Can I use it with Go?", "Can I use it with Java?", "Can my Ruby project use it?". I ain't got no time for this. I need to take a little break and play CS2 with the boys! I looked at these emails. I looked at the ceiling. I looked at my coffee. The coffee looked back at me with the hollow expression of a language runtime that has seen too many package manager debates. And then I cracked my knuckles and got to work. π΅οΈ The Mossad Agents Return I thought I was done with the Mossad agents after 0.2.0. I was not. They came back. Same 3AM knock. Same whiteboard. But this time they brought slides. Twelve of them. With bullet points. And speaker notes. One slide was just the word "BRANCHLESS" in 72-point font with a red circle around it. Their new requirements: memchr for all hot-path string scanning. "You are doing .contains() in a loop like an animal", they said. I was. SmallVec for version component storage. Because heap-allocating a Vec for three numbers (major, minor, patch) is an insult to modern CPU cache lines. Branchless arithmetic for the bump itself. No if major { ... } else if minor { ... } nonsense. One lookup table. One store. Done. Watch mode. They wanted to know the moment a file changed. I asked why. They said that was classified. Auto-detect language manifests. "You scan Cargo.toml like it's the only file in the world", they observed, correctly. "What about pyproject.toml? pom.xml? go.mod? Are Go developers not also suffering?" They are. They are very much also suffering. I implemented every point. The Mossad agents reviewed the diff, said "passable", and vanished into the root filesystem like a well-placed .gitignore entry. β‘ The 1,000,000Γ Number Let me be absolutely scientifically honest with you for one sentence before I stop being honest: the 1,000,000Γ number compares the full bump-my-version CLI round-trip (importing Python, loading dependencies, spawning a subprocess) against our in-process library call with a warm cache and a SmallVec. One is a sports car. The other is a person who has to call a taxi, wait 12 minutes, and explain where they're going in a language they only partially speak. Now let's look at the real numbers. π₯ New Benchmarks: 0.2.0 vs 0.2.1 vs Python Operation bump2version 0.2.0 bump2version 0.2.1 Python pyO3 FFI bump-my-version CLI Parse + bump + serialize ~57 Β΅s ~0.4 Β΅s ~79 Β΅s ~585 ms File search/replace (1k lines) ~65 Β΅s ~11 Β΅s ~1.7 Β΅s ~590 ms File search/replace (100k lines) ~4.2 ms ~0.9 ms N/A ~600 ms Config file parse ~800 Β΅s ~140 Β΅s N/A ~500 ms (on import) Versus the Python CLI subprocess: ((500 x 1000) Γ· 0.4) = ~1,250,000Γ faster for a version parse-bump-serialize. We round down to 1,000,000x for humility. The Mossad agents said rounding up was acceptable. We preferred honesty. What got fast and why: memchr replaced every inner-loop string search. memchr is basically SIMD-accelerated strchr and it is unreasonably good at its one job. SmallVec means version component vectors never touch the allocator for reasonable semver (β€8 parts). Stack-allocated. L1 cache. Zero malloc. Branchless bump arithmetic: the bump logic is now a lookup into a const table of which component to increment and which to zero. The CPU's branch predictor doesn't even have to sweat. Arc cache from 0.2.0 was kept. The regex is compiled exactly once and shared. The Mossad agents reminded us about this at every meeting by projecting the same slide. π‘ Watch Mode bump --watch --bump patch It sounds simple. It was the opposite of simple. "Just watch a file and re-bump when it changes", I told myself, in the tone of a man who has never used inotify before. Bro, I used inotify. Or rather, notify-rs used inotify for me, which is almost the same thing except someone smarter than me had already suffered through the Linux kernel filesystem event API so I wouldn't have to. God bless crate authors. Watch mode works like this: You provide a --watch flag. bump registers a recursive file watcher on all files listed in your .bumpversion.toml. Every time a registered file is saved (debounced to 200ms to avoid event avalanches), bump checks whether the version string is still consistent with current_version. If it detects drift, it re-applies the bump. This is either extremely useful for local development workflows or a deeply irresponsible footgun. We ship tools for adults. The lock safety is your problem. # Watches VERSION, Cargo.toml, and README.md for changes, auto-bumps on save bump --watch --bump patch --config-file .bumpversion.toml # Oh no did I just? yes, yes you did. It already ran. [watch] Detected change: Cargo.toml [watch] Bumping patch: 0.2.1 β 0.2.2 π Multi-Language Auto-Detection: --detect Is Doing God's Work Previous bump2version was Rust-centric. You gave it files, it bumped files. Very obedient lil tool. Very rigid. Like a Rust compiler, actually. The new --detect flag changes all that: bump --bump minor --detect This single command: Walks the entire directory tree (skipping target/, node_modules/, .git/, etc.) Finds every recognized manifest: Cargo.toml, pyproject.toml, setup.cfg, package.json, go.mod, pom.xml, build.gradle, Gemfile Checks each one for the current version string Rewrites every match in one coordinated pass Six languages. One command. cd my-monorepo bump --current-version 0.1.0 --bump patch --detect --dry-run # [detect][dry-run] Would update: Cargo.toml # [detect][dry-run] Would update: pyproject.toml # [detect][dry-run] Would update: package.json # [detect][dry-run] Would update: pom.xml # [detect][dry-run] Would update: go.mod # [detect][dry-run] Would commit 5 file(s): 0.1.0 β 0.1.1 Full supported matrix: Language Manifest Files π¦ Rust Cargo.toml π Python pyproject.toml, setup.cfg, setup.py π¨ JavaScript / Node.js package.json πΉ Go go.mod β Java pom.xml, build.gradle, build.gradle.kts π Ruby Gemfile The Go developers, in particular, messaged to say thank you. We thanked them for using Go despite everything. π Real Language Examples Every language now has a working example under examples/, each with its own pre-baked .bumpversion.toml. # Python project bump --config-file examples/python/.bumpversion.toml --bump patch --dry-run # [dry-run] Would commit 2 file(s): 0.1.0 β 0.1.1 # Node.js project bump --config-file examples/nodejs/.bumpversion.toml --bump minor --dry-run # [dry-run] Would commit 1 file(s): 0.1.0 β 0.2.0 # Java Maven project bump --config-file examples/java/.bumpversion.toml --new-version 2.0.0 --dry-run # [dry-run] Would commit 1 file(s): 0.1.0 β 2.0.0 # Ruby gemspec + Gemfile bump --config-file examples/ruby/.bumpversion.toml --bump major --dry-run # [dry-run] Would commit 2 file(s): 0.1.0 β 1.0.0 # The "bump everything at once" grand finale cd examples/multi-lang bump --current-version 0.1.0 --bump minor --detect --dry-run # [detect][dry-run] Would update: Cargo.toml, pyproject.toml, package.json, pom.xml, Gemfile, VERSION 6 ecosystems. 1 tool. Still forbids unsafe. We have standards. πΈοΈ The Yew WASM App You ever look at a perfectly good CLI tool and think: "This would be better if I could bump versions from a browser"? No? Me neither. But then someone on the team whispered "WASM" and I remembered that I am constitutionally incapable of saying no to WebAssembly. So now there is a Yew-based web application at examples/yew-app/. It: Runs entirely in the browser: wasm32-unknown-unknown, no server required Connects to the same Rust core via a version_ops.rs bridge module And yes, git-related features had to be extracted into an optional feature flag (git) because gix uses Unix-specific APIs that don't compile to WASM. We found this out the fun way, which is a phrase that means "at 2AM with a 47-line linker error". π€ I Abused ChatGPT, Claude and Gemini. Again. More. You may recall from my previous post that I abused Claude during 0.2.0 development. My lawyer said not to bring it up again. So I won't bring up the fact that during 0.2.1 development, I made ChatGPT, Claude and Gemini: Rewrite the detect.rs walk implementation 7 times until it correctly skipped node_modules without accidentally also skipping node_modules_backup (important distinction) Argue with itself about whether SmallVec was better than SmallVec for typical semver usage (it is 8, empirically) Generate .bumpversion.toml configs for 7 languages, review them for correctness, then find its own mistake in the Ruby config and fix it without being asked The OpenAI, Anthropic and Google lawyers have upgraded from "concerned" to "a medium-sized incident report has been filed". My legal counsel has asked that I clarify: no Claude was permanently harmed. Tokens were consumed. Electricity was used. The --detect flag works correctly. π 3 Binaries, One Package, Zero Confusing Changelog Entries After cargo install bump2version --features rust-binary, you get three things: Binary Use case bump The modern, primary CLI cargo-bump Cargo subcommand (cargo bump --bump patch) bump2version β Backward-compatible alias for old CI configs, tutorials, and the 47 blog posts that told people to run bump2version --bump patch The bump2version binary is not deprecated. It is not going anywhere. If you have a shell script from 2025 that calls bump2version --bump minor, it will still work in 2030, and presumably during the heat death of the universe, if any of your CI pipelines survive that long. This was a deliberate choice. Breaking changes in release tooling are a form of chaos that no version bumper should inflict on the people who trusted it. # All of these do the exact same thing: bump --bump patch cargo bump --bump patch bump2version --bump patch # kept for backward compatibility π The .bumpversion.toml Glow-Up The config file now tracks all version-carrying files in the project, not just Cargo.toml: [bumpversion] current_version = 0.2.1 commit = false tag = false [bumpversion:file:Cargo.toml] search = version = "{current_version}" replace = version = "{new_version}" [bumpversion:file:package.json] search = "version": "{current_version}" replace = "version": "{new_version}" [bumpversion:file:README.md] search = "{current_version}" replace = "{new_version}" [bumpversion:file:RUST.md] search = "{current_version}" replace = "{new_version}" [bumpversion:file:WASM.md] search = "{current_version}" replace = "{new_version}" [bumpversion:file:DOCKER.md] search = `{current_version}` replace = `{new_version}` [bumpversion:file:PACKAGING.md] search = {current_version} replace = {new_version} [bumpversion:file:rpm/bump2version.spec] search = Version: {current_version} replace = Version: {new_version} One bump --bump patch and every single version string, across docs, code, configs, packaging specs, and Docker manifests, updates atomically. The Mossad agents reviewed this config and said "acceptable" which, from them, is essentially a standing ovation. π¦ The Borrow Checker Tried to Ruin Watch Mode There is a moment in every Rust developer's life, we all know the moment, where you look at a piece of code that should work, that does work in your head, that you have drawn on paper with arrows and boxes to prove its correctness, and the borrow checker looks at you across the compiler output and says: error[E0505]: cannot move out of 'watcher' because it is borrowed And then below that: note: the borrow later used here And then below that, a footnote that reads note: move occurs because... followed by a chain of reasoning so long it wraps around to the next terminal page. The Yew WASM integration did this to me for different reasons: gix depends on Unix syscalls (openat, statx, platform symlink handling) that simply do not exist in wasm32-unknown-unknown. The linker error was 47 lines long and named three crates I had never heard of. The fix: gating all git-related functionality behind a git feature flag that is disabled by default for WASM builds. Clean. Simple. The kind of solution that is obvious in retrospect and invisible before you spend four hours in the linker output. [features] default = ["std", "git"] git = ["gix", "std"] cli = ["clap", "git"] rust-binary = ["cli", "git", "detect", "watch"] # β full featured binary watch = ["notify", "cli"] detect = ["walkdir", "cli"] The WASM app uses none of the git features. The CLI binary uses all of them. The feature graph is clean enough that my Mossad advisors called it "elegant" before immediately asking about the benchmark numbers. π¦ Getting The Full Picture # The complete CLI, with everything cargo install bump2version --features rust-binary This gets you bump, cargo-bump, and bump2version. All 3. No choices required. Just install and bump. Quick Examples # Standard semver bump --bump patch # 0.2.1 β 0.2.2 bump --bump minor # 0.2.1 β 0.3.0 bump --bump major # 0.2.1 β 1.0.0 # Safe preview bump --bump patch --dry-run bump --bump minor -n # same thing, shorter # Git integration bump --bump patch --commit --tag # Custom message bump --bump minor --commit --message "chore: ship {new_version} π" # Auto-detect all manifests bump --bump patch --detect # Watch mode (exits when you ctrl + c) bump --watch --bump patch As a library (no std required for the core) [dependencies] bump2version = { version = "0.2.1", default-features = false } use bump2version::{config::BumpConfig, version::{parse_version, bump_version, serialize_version}}; let cfg = BumpConfig::default(); let v = parse_version("1.2.3", &cfg).unwrap(); let v2 = bump_version(&v, "minor", &cfg).unwrap(); assert_eq!(serialize_version(&v2, &cfg), "1.3.0"); Python pip install bump-rs from bump_rs import bump_version print(bump_version("1.2.3", "major")) # "2.0.0" in ~57Β΅s Node.js npm install bump2version const { bumpVersion } = require("bump2version"); console.log(bumpVersion("1.2.3", "patch")); // "1.2.4" π What Comes After 1,000,000Γ Look, at some point the law of diminishing returns kicks in. We cannot make version bumping faster than the speed of light. (We checked. We asked the Mossad agents. They checked their slides. The answer was no.) But there are things we can still do: Pre-release cycling: proper alpha.1 β alpha.2 β beta.1 β rc.1 β stable lifecycle, first-class Workspace-aware bumping: atomic multi-crate Cargo workspace updates, all in one commit More detect targets: .NET (*.csproj), PHP (composer.json), Swift (Package.swift), Elixir (mix.exs) Browser version history: the Yew app keeps a local bump history so you can see what you broke and when If you want any of these sooner: open an issue. Or just star the repo and the moral pressure will accelerate delivery. It works on me. I've tested this empirically. π¬ Closing Thoughts bump2version 0.2.1 started as a performance exercise and became an ecosystem. What was a single binary is now: A Rust library with no_std support for the core A Python package (bump-rs) for Pythonistas who want the speed without the syntax A Node.js native add-on for JavaScript developers who want to feel like they're using Rust A CLI with three binary names: bump, cargo-bump, bump2version A WASM browser app built in Yew, because someone had to 7 language examples covering Rust, Python, Node.js, Go, Java, Ruby, and poly-repo workflows 20 integration tests that test the actual binary, not a mock An auto-updatable .bumpversion.toml that tracks all version strings across the entire project cargo install bump2version --features rust-binary β bump β ship β repeat β be 1,000,000Γ faster than Python β sleep β repeat π¦ wiseaidev / bump2version β¬οΈ A blazingly fast, thread safe, git client agnostic, CLI for managing version numbers in your projects. β¬οΈ Bump2version bump2version is the world's fastest version bumper written entirely in 100% safe Rust, with no_std support, native Python and Node.js bindings, and a cargo bump subcommand πΏ. π Installation Platform Command Rust binary cargo install bump2version --features rust-binary Cargo subcommand cargo bump --help Docker docker pull wiseaidev/bump2version Debian/Ubuntu Download .deb from GitHub Releases RHEL/Fedora Download .rpm from GitHub Releases Windows Download bump.exe from GitHub Releases GitHub Action See action.yml Python pip install bump-rs Node.js npm install bump2version Note Installing via cargo installs both bump and bump2version binaries. The original bump2version binary is retained indefinitely for backward compatibility with existing tutorials, CI/CD pipelines, and automation scripts. π€ What does this crate provide? bump2version automates semantic version management for any project regardless of language. It: Parses version strings using a fully configurable regex (default: semver major.minor.patch). Bumps any named component (major, minor, patch, orβ¦ View on GitHub Star the repo. Try the Python bindings. Use the Node.js package. Read the Rust docs. Poke the WASM demo. Run the examples. This has been a public service announcement from a developer who, in the course of a single engineering session, created 20 tests, 7 example projects, a WASM frontend, a Soviet-themed benchmark suite, and consumed an amount of Claude tokens that my accountant has asked me not to disclose. The legal proceedings with Top Tech companies remain ongoing. My lawyer has read this post. He has asked me to note, for the record, that "abusing Claude" is a colloquial and affectionate term and not a legally actionable description of token consumption. I have not taken that advice either. Till next time: Keep bumpin', keep rustin', keep benchmarkin' π¦β¬οΈ P.S. The Mossad agents have approved this post subject to the removal of the classified section about the branchless arithmetic lookup table. We kept it in. They know. They've said nothing. This is ominous.
This is a summary aggregated from Dev.to. Read the complete article on the original site:
Read full article at Dev.to