Skip to content
Open
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
9 changes: 7 additions & 2 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,13 @@
BINARY_NAME=late
VERSION?=2.0.0-rc.1

# Go compiler flags
LDFLAGS=-ldflags "-X late/internal/common.Version=${VERSION}"
# Go compiler flags. Commit/build-number/build-date stamping: when git is
# unavailable (tarball checkout, no repo) the commit and build number
# degrade to "unknown" without failing the build.
GIT_COMMIT:=$(shell git rev-parse --short HEAD 2>/dev/null || echo unknown)
BUILD_NUMBER:=$(shell git rev-list --count HEAD 2>/dev/null || echo unknown)
BUILD_DATE:=$(shell date -u +%Y-%m-%dT%H:%M:%SZ)
LDFLAGS=-ldflags "-X late/internal/common.Version=${VERSION} -X late/internal/common.BuildNumber=${BUILD_NUMBER} -X late/internal/common.Commit=${GIT_COMMIT} -X 'late/internal/common.BuildDate=${BUILD_DATE}'"

help: ## Show this help
@awk 'BEGIN {FS = ":.*?## "} /^[a-zA-Z_-]+:.*?## / {printf "\033[36m%-15s\033[0m %s\n", $$1, $$2}' $(MAKEFILE_LIST)
Expand Down
4 changes: 3 additions & 1 deletion cmd/late/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -128,7 +128,9 @@ func main() {
tool.SetSqzEnabled(*enableSqzReq)

if *versionReq {
fmt.Printf("late %s\n", common.Version)
// Full one-line build identity: version + stamped commit/build
// date, degrading to the bare dev banner for a plain `go build`.
fmt.Println(common.VersionDisplay())
return
}

Expand Down
90 changes: 90 additions & 0 deletions internal/common/version.go
Original file line number Diff line number Diff line change
@@ -1,3 +1,93 @@
package common

import "strings"

// Version is the release identifier (e.g. "2.0.0-rc.1"), set via
// -ldflags -X at build time; it stays "dev" for a plain `go build`.
var Version = "dev"

// BuildNumber is the deterministic build ordinal — the commit count of the
// current HEAD (`git rev-list --count HEAD`), set via -ldflags -X at build
// time (Makefile: BUILD_NUMBER). It is stable for the same commit, grows
// monotonically as the repo grows, and needs no network; it stays "unknown"
// when not stamped (plain `go build`) or when git is unavailable (tarball
// without .git).
var BuildNumber = "unknown"

// Commit is the short git commit the binary was built from, set via
// -ldflags -X at build time (Makefile: `git rev-parse --short HEAD`);
// it stays "unknown" when not stamped (plain `go build`, tarball
// without .git).
var Commit = "unknown"

// BuildDate is the UTC build timestamp (RFC3339), set via -ldflags -X
// at build time (Makefile: `date -u +%Y-%m-%dT%H:%M:%SZ`); it stays
// "unknown" when not stamped.
var BuildDate = "unknown"

// versionDisplay renders the one-line build identity from injected
// values, so tests can exercise every degradation without mutating the
// package vars:
//
// late 2.0.0-rc.1 (build 1239, commit 2fe0e83, built 2026-09-25T13:12:11Z)
//
// Degradations (the output stays on one line):
// - unknown build number ("unknown" or empty) → the build number is omitted;
// - unknown commit ("unknown" or empty) → the commit is omitted;
// - unknown date ("unknown" or empty) → the date is omitted;
// - dev version with nothing stamped (plain `go build`) → bare "late dev".
func versionDisplay(version, buildNumber, commit, buildDate string) string {
buildKnown := buildNumber != "" && buildNumber != "unknown"
commitKnown := commit != "" && commit != "unknown"
dateKnown := buildDate != "" && buildDate != "unknown"
if version == "dev" && !buildKnown && !commitKnown && !dateKnown {
return "late dev"
}
display := "late " + version
var meta []string
if buildKnown {
meta = append(meta, "build "+buildNumber)
}
if commitKnown {
meta = append(meta, "commit "+commit)
}
if dateKnown {
meta = append(meta, "built "+buildDate)
}
if len(meta) > 0 {
display += " (" + strings.Join(meta, ", ") + ")"
}
return display
}

// VersionDisplay renders the full one-line build identity for -version
// from the stamped package vars. See versionDisplay for the format and
// its graceful degradations.
func VersionDisplay() string {
return versionDisplay(Version, BuildNumber, Commit, BuildDate)
}

// versionDisplayShort renders the compact identity used where only one
// row must fit (the TUI info bar): "<version>[ · b<build number>][ ·
// <commit>]" — the build number comes first, then the short commit. Each
// unknown piece degrades silently to just the version, and the build date
// is deliberately left out.
func versionDisplayShort(version, buildNumber, commit string) string {
parts := make([]string, 0, 3)
if version != "" {
parts = append(parts, version)
}
if buildNumber != "" && buildNumber != "unknown" {
parts = append(parts, "b"+buildNumber)
}
if commit != "" && commit != "unknown" {
parts = append(parts, commit)
}
return strings.Join(parts, " · ")
}

// VersionDisplayShort renders the short build identity from the stamped
// package vars. See versionDisplayShort for the format.
func VersionDisplayShort() string {
return versionDisplayShort(Version, BuildNumber, Commit)
}
211 changes: 211 additions & 0 deletions internal/common/version_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,211 @@
package common

import (
"strings"
"testing"
)

func TestVersionDisplay(t *testing.T) {
tests := []struct {
name string
version string
buildNum string
commit string
buildDate string
want string
}{
{
name: "all four pieces stamped, order is build, commit, built",
version: "2.0.0-rc.1",
buildNum: "1239",
commit: "2fe0e83",
buildDate: "2026-09-25T13:12:11Z",
want: "late 2.0.0-rc.1 (build 1239, commit 2fe0e83, built 2026-09-25T13:12:11Z)",
},
{
name: "build number only",
version: "2.0.0-rc.1",
buildNum: "1239",
want: "late 2.0.0-rc.1 (build 1239)",
},
{
name: "build number and build date omit the commit",
version: "2.0.0-rc.1",
buildNum: "1239",
buildDate: "2026-09-25T13:12:11Z",
want: "late 2.0.0-rc.1 (build 1239, built 2026-09-25T13:12:11Z)",
},
{
name: "commit and build date stamped without build number",
version: "2.0.0-rc.1",
commit: "2fe0e83",
buildDate: "2026-09-25T10:57:00+02:00",
want: "late 2.0.0-rc.1 (commit 2fe0e83, built 2026-09-25T10:57:00+02:00)",
},
{
name: "commit only",
version: "2.0.0-rc.1",
commit: "2fe0e83",
want: "late 2.0.0-rc.1 (commit 2fe0e83)",
},
{
name: "build date only",
version: "2.0.0-rc.1",
buildDate: "2026-09-25T08:57:00Z",
want: "late 2.0.0-rc.1 (built 2026-09-25T08:57:00Z)",
},
{
name: "nothing stamped",
version: "2.0.0-rc.1",
want: "late 2.0.0-rc.1",
},
{
name: "dev with nothing stamped is the bare dev banner",
version: "dev",
want: "late dev",
},
{
name: "unknown sentinels are treated as unstamped",
version: "2.0.0-rc.1",
buildNum: "unknown",
commit: "unknown",
buildDate: "unknown",
want: "late 2.0.0-rc.1",
},
{
name: "empty build number is treated as unstamped",
version: "2.0.0-rc.1",
buildNum: "",
commit: "2fe0e83",
want: "late 2.0.0-rc.1 (commit 2fe0e83)",
},
{
name: "dev with only the build number stamped keeps it",
version: "dev",
buildNum: "1239",
want: "late dev (build 1239)",
},
{
name: "dev with only the build date stamped keeps the date",
version: "dev",
buildDate: "2026-09-25T08:57:00Z",
want: "late dev (built 2026-09-25T08:57:00Z)",
},
{
name: "dev with only the commit stamped keeps the commit",
version: "dev",
commit: "2fe0e83",
want: "late dev (commit 2fe0e83)",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got := versionDisplay(tt.version, tt.buildNum, tt.commit, tt.buildDate)
if got != tt.want {
t.Errorf("versionDisplay(%q, %q, %q, %q) = %q, want %q", tt.version, tt.buildNum, tt.commit, tt.buildDate, got, tt.want)
}
// The output must stay on one line: it feeds -version stdout
// and single-row parsers.
if strings.ContainsAny(got, "\n\r") {
t.Errorf("versionDisplay(...) = %q, must not contain line breaks", got)
}
})
}
}

func TestVersionDisplayShort(t *testing.T) {
tests := []struct {
name string
version string
buildNum string
commit string
want string
}{
{
name: "build number precedes the commit",
version: "2.0.0-rc.1",
buildNum: "1239",
commit: "2fe0e83",
want: "2.0.0-rc.1 · b1239 · 2fe0e83",
},
{
name: "build number only",
version: "2.0.0-rc.1",
buildNum: "1239",
want: "2.0.0-rc.1 · b1239",
},
{
name: "commit only",
version: "2.0.0-rc.1",
commit: "2fe0e83",
want: "2.0.0-rc.1 · 2fe0e83",
},
{
name: "unknown build number degrades to version and commit",
version: "2.0.0-rc.1",
buildNum: "unknown",
commit: "2fe0e83",
want: "2.0.0-rc.1 · 2fe0e83",
},
{
name: "empty build number degrades to version and commit",
version: "2.0.0-rc.1",
buildNum: "",
commit: "2fe0e83",
want: "2.0.0-rc.1 · 2fe0e83",
},
{
name: "unknown commit degrades to the bare version",
version: "2.0.0-rc.1",
commit: "unknown",
want: "2.0.0-rc.1",
},
{
name: "empty commit degrades to the bare version",
version: "2.0.0-rc.1",
want: "2.0.0-rc.1",
},
{
name: "dev with unknown commit",
version: "dev",
want: "dev",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got := versionDisplayShort(tt.version, tt.buildNum, tt.commit)
if got != tt.want {
t.Errorf("versionDisplayShort(%q, %q, %q) = %q, want %q", tt.version, tt.buildNum, tt.commit, got, tt.want)
}
if strings.ContainsAny(got, "\n\r") {
t.Errorf("versionDisplayShort(...) = %q, must not contain line breaks", got)
}
})
}
}

// TestVersionDisplayWrappersUseVars pins that the thin var-backed wrappers
// plumb the package vars through (the build-tag-free injection point used
// by the TUI tests).
func TestVersionDisplayWrappersUseVars(t *testing.T) {
origVersion, origBuildNum, origCommit, origBuildDate := Version, BuildNumber, Commit, BuildDate
defer func() { Version, BuildNumber, Commit, BuildDate = origVersion, origBuildNum, origCommit, origBuildDate }()

Version, BuildNumber, Commit, BuildDate = "2.0.0-rc.1", "1239", "2fe0e83", "2026-09-25T10:57:00+02:00"
if got, want := VersionDisplay(), "late 2.0.0-rc.1 (build 1239, commit 2fe0e83, built 2026-09-25T10:57:00+02:00)"; got != want {
t.Errorf("VersionDisplay() = %q, want %q", got, want)
}
if got, want := VersionDisplayShort(), "2.0.0-rc.1 · b1239 · 2fe0e83"; got != want {
t.Errorf("VersionDisplayShort() = %q, want %q", got, want)
}

// A plain `go build` (all vars at their source defaults) is the bare
// dev banner.
Version, BuildNumber, Commit, BuildDate = "dev", "unknown", "unknown", "unknown"
if got := VersionDisplay(); got != "late dev" {
t.Errorf("VersionDisplay() = %q, want %q", got, "late dev")
}
if got := VersionDisplayShort(); got != "dev" {
t.Errorf("VersionDisplayShort() = %q, want %q", got, "dev")
}
}
Loading