1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
//! Advisory `min_bl_version` check (SPEC §5 / §10).
//!
//! Purely advisory. Older `bl` clients ignore the field structurally —
//! `Config` carries no `deny_unknown_fields`, so a pre-this-spec binary
//! drops it on load and there is nothing to engineer. A client that
//! *does* know the field warns when its own build is below the floor a
//! repo requests. This is the documented (not engineered) mitigation
//! for the single accepted BC caveat: an old `bl` on a deferred-mode
//! repo still local-squashes on `bl review`, contaminating the
//! integration branch (epic bl-00ca ACCEPTED CAVEAT 2026-05-10). We
//! nudge; we do not prevent.
//!
//! No semver crate: `bl` versions are plain `MAJOR.MINOR.PATCH` and a
//! three-int compare suffices. Operator shorthand (`0.4` ⇒ `0.4.0`) is
//! accepted; anything else is reported as malformed and the advisory is
//! ignored — an ineffective floor the operator should hear about.
/// The version this `bl` binary was built at.
pub fn running_version() -> &'static str {
env!("CARGO_PKG_VERSION")
}
/// Parse `MAJOR[.MINOR[.PATCH]]` into a comparable triple. Absent
/// trailing components default to `0`. `None` for more than three
/// components or any non-numeric component (covers the empty string,
/// which splits to a single unparseable element).
fn parse_triple(v: &str) -> Option<(u64, u64, u64)> {
let parts: Vec<&str> = v.split('.').collect();
if parts.len() > 3 {
return None;
}
let mut nums = [0u64; 3];
for (i, p) in parts.iter().enumerate() {
nums[i] = p.parse::<u64>().ok()?;
}
Some((nums[0], nums[1], nums[2]))
}
/// Pure core: given the configured floor and the running version,
/// return the line to print, or `None` when nothing should be said.
/// Split from I/O so every branch is unit-testable without capturing
/// stderr. A floor that won't parse yields its own advisory; a running
/// version that won't parse silently says nothing (can't compare).
pub fn advisory(min: Option<&str>, running: &str) -> Option<String> {
let min = min?;
let Some(floor) = parse_triple(min) else {
return Some(format!(
"warning: config min_bl_version {min:?} is not a valid \
MAJOR.MINOR.PATCH version; advisory ignored"
));
};
let here = parse_triple(running)?;
if here < floor {
Some(format!(
"warning: this bl is {running}; this repo requests \
min_bl_version {min}. An older bl on a deferred-mode repo \
local-squashes `bl review` and contaminates the \
integration branch (SPEC §10). Upgrade bl. (advisory only)"
))
} else {
None
}
}
/// Emit the advisory to stderr if there is one. The only side-effecting
/// entry point; `Config::load` calls it after a successful parse so the
/// nudge rides every command that touches a configured repo.
pub fn warn_if_below(min: Option<&str>) {
if let Some(msg) = advisory(min, running_version()) {
eprintln!("{msg}");
}
}
#[cfg(test)]
#[path = "min_version_tests.rs"]
mod tests;