Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
32 changes: 27 additions & 5 deletions keel/commands/tui.py
Original file line number Diff line number Diff line change
Expand Up @@ -136,12 +136,34 @@ def _human_dt(ts: int) -> str:


def _short_version(raw: str) -> str:
"""`v<major>.<minor>` from a full version string (`0.1.0` -> `v0.1`). Falls back to `v?` for an
unknown or unparseable version, so the header never shows a bare `unknown` or raises."""
"""`v<major>.<minor>.<patch>` from a full version string (`0.5.2` -> `v0.5.2`).

The patch segment is shown because major.minor alone cannot tell two deployments apart: every
release in the 0.5 line rendered as `v0.5`, so an operator glancing at the header could not
see whether the box was running the build they just shipped or the one before it.

Three degradations, each preferring the most information it can still vouch for:

* **Build metadata is stripped from the patch.** A release version is `0.5.2+79f35b9e73d5`, so
`parts[2]` is `2+79f35b9e73d5` -- not a digit. Taking it verbatim would print the whole
commit hash into a header line budgeted for a version; rejecting it would drop the patch on
precisely the shape a released build emits. Split on `+` and keep the numeric head.
* **No patch segment** (`2.0`) -> `v2.0`. Show what exists rather than inventing a `.0` the
version string never claimed.
* **A non-numeric patch** (`0.5.2rc1`) -> `v0.5`. Falling all the way back to `v?` would throw
away the two segments that did parse.

Falls back to `v?` only when major/minor themselves are unparseable, so the header never shows
a bare `unknown` and never raises.
"""
parts = raw.split(".")
if len(parts) >= 2 and parts[0].isdigit() and parts[1].isdigit():
return f"v{parts[0]}.{parts[1]}"
return "v?"
if len(parts) < 2 or not parts[0].isdigit() or not parts[1].isdigit():
return "v?"
head = f"v{parts[0]}.{parts[1]}"
if len(parts) < 3:
return head
patch = parts[2].split("+", 1)[0]
return f"{head}.{patch}" if patch.isdigit() else head


#: The short header version, resolved ONCE at import (it never changes within a run). Uses the
Expand Down
15 changes: 12 additions & 3 deletions tests/commands/test_tui.py
Original file line number Diff line number Diff line change
Expand Up @@ -424,10 +424,19 @@ def test_build_screen_title_shows_version_and_mode_without_datetime() -> None:
@pytest.mark.parametrize(
"raw,expected",
[
("0.1.0", "v0.1"),
("0.2.5", "v0.2"),
("0.1.0", "v0.1.0"),
("0.2.5", "v0.2.5"),
("0.5.2", "v0.5.2"),
# Build metadata rides on the PATCH segment (`0.5.2+79f35b9e73d5`), so a naive
# `parts[2].isdigit()` is False and the patch would silently vanish -- which is exactly
# the shape `keel --version` emits, i.e. the common case, not the edge case.
("10.34.1+abc", "v10.34.1"),
("0.5.2+79f35b9e73d5", "v0.5.2"),
# No patch segment at all: show what exists rather than inventing a `.0`.
("2.0", "v2.0"),
("10.34.1+abc", "v10.34"),
# A non-numeric patch (pre-release) degrades to major.minor -- still useful -- rather
# than to `v?`, which would throw away the two segments we did parse.
("0.5.2rc1", "v0.5"),
("unknown", "v?"),
("1", "v?"),
("", "v?"),
Expand Down
Loading