嗨,各位版本升級愛好者、睡眠不足的 Rustacean,以及剛發現
bumpversion這東西的意外軟體考古學家們 👋!
當時我正盯著終端機看,時間是凌晨 2 點,試著發布某個東西的 0.1.0 版本。我輸入了 bump-my-version patch,按下 Enter,然後看著 CPU 風扇像要發射 SpaceX 火箭一樣轉了起來。一秒後,一秒而已,它只升了一個數字。就一個小小的數字。0.1.0 → 0.1.1。
我沉默地坐了一會兒。
接著我做了任何理性開發者都會做的事:我用 Rust 重新寫了一遍。從零開始。還做了 Python 和 Node.js 的綁定。還有 CLI。還有 no_std 支援。還用了 gix 做純 Rust 的 git 操作。
結果呢?bump2version 0.2.0:一個版本升級工具,速度比它取代的 Python CLI 真的、可測量地、令人尷尬地快了約 10,000 倍。

bump2version 到底是什麼?很高興你問了。bump2version 會自動處理發佈軟體時最瑣碎的部分:更新多個檔案中的版本字串。你知道的,就是那種你手動 grep Cargo.toml、package.json、pyproject.toml、CHANGELOG.md 和 README,把 11 個不同地方的 1.2.3 改成 1.2.4,結果漏掉一個,push 上去,CI 爆掉,然後你默默對著咖啡哭泣的那種事。
對,就是那個部分。
bump2version 幫你把這些全包了:
major.minor.patch)。major、minor、patch,或像 alpha → beta → stable 這種自訂循環階段。(?ms) DOTALL + MULTILINE 語義的多行 CHANGELOG 模式。gix 提交與標記——100% 純 Rust 的 git,零子程序呼叫,提交歷史裡也不會冒出幽靈作者。而且這一切都在 安全 Rust 中完成,crate 根目錄還加了 #![forbid(unsafe_code)],因為我們這裡是有原則的。至少我們假裝有。
# .bumpversion.toml:真的會幫你升對版本的設定檔
[bumpversion]
current_version = "0.2.0"
commit = true
tag = true
[bumpversion:file:Cargo.toml]
search = 'version = "{current_version}"'
replace = 'version = "{new_version}"'
[bumpversion:file:CHANGELOG.md]
search = "## {current_version}\n Release notes line 1"
replace = "## {new_version}\n Release notes line 1"
一個設定檔。多個檔案更新。一個 git commit。一個 tag。完成。

有趣的地方來了:bump2version 不只是 Rust crate。它是三個工具假裝自己是同一個,還披著風衣站在一起。
[dependencies]
bump2version = "0.2.0"
use bump2version::{config::BumpConfig, version::{parse_version, bump_version, serialize_version}};
fn main() {
let cfg = BumpConfig::default();
let v = parse_version("1.2.3", &cfg).unwrap();
let v2 = bump_version(&v, "patch", &cfg).unwrap();
println!("{}", serialize_version(&v2, &cfg)); // 1.2.4
}
pip install bump-rs
from bump_rs import bump_version, BumpConfig
print(bump_version("1.2.3", "patch")) # "1.2.4"
print(bump_version("1.2.3", "minor")) # "1.3.0"
print(bump_version("1.2.3", "major")) # "2.0.0"
npm install bump2version
const { bumpVersion, applyFileChange } = require("bump2version");
console.log(bumpVersion("1.2.3", "patch")); // '1.2.4'
console.log(bumpVersion("1.2.3", "minor")); // '1.3.0'
一個 Rust 核心。三個生態系。零個 Python 子程序。Ferris 這隻蟹現在會多種語言了,而老實說?替它開心。🦀
我先坦白一件事:這個專案的完整系統設計不是我一個人架構的。
沒有,我有幫手。更精確地說,我找了幾位非常專業的顧問。

他們凌晨 3 點帶著白板來我家門口,還對 Arc<Regex> 快取策略有非常詳細的意見。 我照單全收的關鍵架建置議,就是那個執行緒安全的 Arc<Regex> 快取。這代表編譯後的正規表示式只會編譯 一次,然後在各執行緒之間共享,後續每次呼叫都重複使用,熱路徑上不會有重新編譯的負擔。
結果:從 Python 端執行版本升級只要 約 57 微秒。不是 57 毫秒。不是 57 秒。是 57 微秒。這種數字會讓你忍不住想,Python 版本那 585 毫秒到底都在做什麼。
好了,來談基準測試。因為這是部落格文章中,我可以貼表格然後深深覺得自己很有優越感的部分。
這些都是真實資料,在 x86-64 Linux 上測得(CPython 3.12,經過 3-sigma 過濾的 timeit):
| 函式庫 | patch |
minor |
major |
|---|---|---|---|
bump-rs(Rust,Arc<Regex> 快取) |
~57 µs | ~54 µs | ~53 µs |
bump-my-version(Python 函式庫) |
~79 µs | ~95 µs | ~72 µs |
純 Python(re.compile + int()) |
~3.6 µs | ~2.2 µs | ~2.2 µs |
bump-my-version CLI(子程序) |
~585 ms | ~585 ms | ~585 ms |
重點結果:bump-rs 比 bump-my-version CLI 快約 10,000 倍。
我已經聽到你想說什麼了:「可是純 Python 版本在單次呼叫時其實更快!」
沒錯。你說得對。約 ~50 µs 的 PyO3 FFI 開銷意味著,如果你只是把一個版本字串獨立地升級一次,而且 Python 解譯器還是暖的,那純 re.compile + int() 會直接把我們比下去。
但只要你開始做任何真正的事:解析設定檔、更新多個檔案、跑 git commit,你就會發現,用 bump-rs 是一次做完;反觀另一邊得啟動子程序、載入 click、載入 importlib、載入整個 bump-my-version 相依圖……然後等待 585 毫秒。
每次都一樣。

| 函式庫 | 單行 | 多行 CHANGELOG |
|---|---|---|
| bump-rs(Rust,已快取) | ~65 µs | ~104 µs |
純 Python re.sub |
~1.7 µs | ~1.3 µs |
對於檔案 I/O、執行緒安全、以及管線操作,bump-rs 會贏。對於那種超小型、單次呼叫、純記憶體操作,而且 FFI 開銷佔主導的情況:請在批次模式使用 bump-rs,或者直接用 Python。我們這裡講求誠實。
這裡我要坦白。是個 非常私人的 坦白。我的法務團隊強烈建議我不要公開說。
我操弄了 Claude。
不是那種 正常 的用法,也不是叫它產生樣板碼。不不不。我把它逼到極限。我叫它用六種不同方式寫同一段正規表示式快取邏輯,直到其中一版不會讓 borrow checker 哭出來。我讓它在凌晨 4 點架構 FFI 邊界語義。我拿它來辯論 Arc<Regex> 對單執行緒基準測試來說是不是大材小用(答案是不是)。我還要它詳細解釋自己的推理,然後再跟它爭論。
Anthropic 注意到了。

我的律師辯稱,我只是在「探索模型能力的完整表面」。法官無動於衷。Anthropic 的律師也無動於衷,只是方向不太一樣。
判決仍未出爐。不過,Arc<Regex> 快取已經可以上線。
這件事的教訓是:如果你想把工具效能榨出 10,000 倍,你得願意走進一些不舒服的地方。黑暗的地方。那些你在凌晨 4 點叫 AI 第七次重寫正規表示式快取,卻真的分不清楚到底是你比較累,還是 token 比較累的地方。
結果發現:token 不會累。這就是 Rust 會贏的原因。

每個 Rust 開發者的人生中,都會有這麼一刻:你寫出一段自己 知道 是正確的程式,你甚至用數學歸納法,還有感覺,已經在腦中證明過它是對的,但 borrow checker 會直直看著你的眼睛說:「不行。」
沒有解釋。沒有建議。只有一個佔了終端機五行的錯誤訊息,而且 somehow 還能讓你覺得自己被編譯器人身攻擊了。
那種事發生了。還不只一次。特別是在 Python binding 層,PyO3 的 GIL 管理、Arc<Regex> 的共享狀態,以及 Rust 的生命週期規則交錯在一起,會產生一種只能用「我頭好痛,我想回家」來形容的特殊混亂。

欄杆上的馬,正好就是 Arc<Mutex<HashMap<String, Regex>>> 試圖跨過 PyO3 函式邊界時的寫照。它最後有過去。它能用。但在它真正能用之前,那個「卡住」的瞬間?是真的。
戲劇性地,修正方式只是把快取從 Mutex 裡包著的 HashMap 改成使用 once_cell::sync::Lazy 初始化的 thread-local Arc<Regex>。borrow checker 立刻、而且很有禮貌地,讓馬從欄杆上下來了。
這裡面大概有個隱喻。我選擇不要太仔細去看。

來點實作。以下是你現在就可以在專案中使用 bump2version 的方法:
cargo install bump2version --features rust-binary
bump2version --bump patch # 0.2.0 → 0.2.1
bump2version --bump minor # 0.2.0 → 0.3.0
bump2version --bump major # 0.2.0 → 1.0.0
實用參數:
| 選項 | 功能 |
|---|---|
--config-file |
指定設定檔路徑 |
--current-version |
覆寫偵測到的目前版本 |
--bump |
要升級哪個部分:major、minor、patch |
--dry-run / -n |
模擬執行,不會動任何檔案 |
--commit / --tag |
升級後自動提交與加上 tag |
pip install bump-rs
from bump_rs import bump_version, apply_file_change, BumpConfig
# 自訂 2 段式版本的解析/序列化
cfg = BumpConfig(parse=r"(?P<major>\d+)\.(?P<minor>\d+)", serialize="{major}.{minor}")
print(bump_version("2.0", "minor", config=cfg)) # "2.1"
npm install bump2version
import { bumpVersion, applyFileChange } from "bump2version";
const next = bumpVersion("1.2.3", "minor"); // "1.3.0"
no_std 嵌入bump2version = { version = "0.2.0", default-features = false }
核心模組(config、version、files、error)都能在 no_std + alloc 下編譯。對也要管理軟體發布週期的微控制器很有用。你知道的,如果你的情況就是這樣。
bump2version 在 crate 根目錄強制啟用 #![forbid(unsafe_code)]。實作、設定解析、正規表示式比對、版本升級、git 物件建立的每一個位元組,都是用安全 Rust 寫成。編譯器甚至會直接 拒絕 之後在安全區域引入的任何 unsafe。
整個程式碼庫裡唯一的 unsafe 出現在 Node.js FFI 層,因為 napi-rs 的原生附加元件互操作就是需要它,而且真的沒辦法繞過。如果可以避免,我們早就避了。我們試過。borrow checker 對我們的努力點頭稱許,然後還是說不行。

bump2version 0.2.0 已經推出,但路線圖還很滿:
alpha → beta → rc → stable 生命週期。講白了,bump2version 就做一件事:幫你把檔案裡的數字升級、提交結果,然後加上 tag。就這樣。功能就這麼多。
但它是用 安全 Rust 寫的。還有 Python 綁定,讓 Python 開發者不用在意細節。還有 Node.js 綁定,讓 JavaScript 開發者也能假裝自己有在用 Rust。還有 no_std 支援,讓嵌入式工程師也能參與版本管理的對話。還有 純 gix 的 git 整合,讓熱路徑上完全沒有任何子程序呼叫。還有基準測試證明它比現成的 CLI 工具 快約 10,000 倍。
拿來升一個數字,這是不是有點過頭?絕對是。那我們後悔嗎?一點也不。
cargo install bump2version --features rust-binary→ 升版 → 發布 → 重複 🦀
把 repo 點星、試試 Python 綁定、安裝 npm 套件,或者直接看看 文件。所有路都會通往更快的版本升級,以及你對發布流程稍微更得意的關係。
https://github.com/wiseaidev/bump2version
以上是一則公共服務公告,來自一位真的、真的不想為了一個數字加 1 而等 585 毫秒的開發者。

我們下次見:繼續 bump,繼續 rust 🦀⬆️
P.S. 我和 Anthropic 的法律程序仍在進行中。我的律師建議我不要再提這件事。我沒有採納這個建議。