Show git commit and build time in vintner version

Uses Go's automatic VCS build-info stamping (vcs.revision/vcs.time/
vcs.modified, embedded by `go build` since Go 1.18 - no -ldflags
changes needed, works the same for a CI release build and a plain
local `go build`). Knowing the exact commit a bug report's binary was
built from, not just the X.Y.Z tag, is the point - two builds of the
same tag could still differ.

Split the pure formatting logic (formatVersion) from the
debug.ReadBuildInfo() call so it's actually unit-testable: `go test`
binaries don't get VCS stamping the way `go build` ones do, so there
was no way to exercise the revision-formatting branch through
versionString() itself.
This commit is contained in:
Cheviiot
2026-07-25 11:37:02 +10:00
parent 6186837fd2
commit 94a19e6b43
3 changed files with 103 additions and 1 deletions
+1 -1
View File
@@ -50,7 +50,7 @@ func runCLI(args []string) int {
case "completion":
return runCompletion(args[1:])
case "version", "v", "--version":
fmt.Println("vintner " + version)
fmt.Println(versionString())
return 0
case "-h", "--help", "help", "h":
printUsage()
+56
View File
@@ -0,0 +1,56 @@
package main
import (
"fmt"
"runtime/debug"
)
// versionString renders "vintner <version>" plus, when available, the git
// commit and build time Go's toolchain embeds automatically (since Go
// 1.18, `go build` stamps vcs.revision/vcs.time/vcs.modified into the
// binary on its own - no -ldflags needed for this part, so it works the
// same whether the binary came from CI or a plain local `go build`).
// Knowing the exact commit a reported bug was built from, not just the
// X.Y.Z tag, is the point: two builds of the same tag could still differ
// if the tag was ever moved, or if someone built from an uncommitted tree.
//
// Note for anyone testing this: `go build` stamps vcs.* build settings,
// but `go test` binaries don't get them - there's no environment where a
// `go test` run can exercise the revision-formatting branch below, hence
// formatVersion is split out and tested directly instead.
func versionString() string {
revision, buildTime, dirty := "", "", false
if info, ok := debug.ReadBuildInfo(); ok {
for _, s := range info.Settings {
switch s.Key {
case "vcs.revision":
revision = s.Value
case "vcs.time":
buildTime = s.Value
case "vcs.modified":
dirty = s.Value == "true"
}
}
}
return formatVersion(version, revision, buildTime, dirty)
}
// formatVersion is the pure part of versionString: given a revision (full
// git SHA, may be empty), it's shortened to 12 chars and marked "-dirty" if
// the build tree had uncommitted changes.
func formatVersion(ver, revision, buildTime string, dirty bool) string {
v := "vintner " + ver
if revision == "" {
return v
}
if len(revision) > 12 {
revision = revision[:12]
}
if dirty {
revision += "-dirty"
}
if buildTime != "" {
return fmt.Sprintf("%s (%s, %s)", v, revision, buildTime)
}
return fmt.Sprintf("%s (%s)", v, revision)
}
+46
View File
@@ -0,0 +1,46 @@
package main
import "testing"
func TestFormatVersion(t *testing.T) {
for _, tc := range []struct {
name string
ver, revision, buildTime string
dirty bool
want string
}{
{
name: "no VCS info at all",
ver: "dev",
want: "vintner dev",
},
{
name: "clean build with full info",
ver: "0.3.0",
revision: "6186837fd23616335ba8aff830801692a756799c",
buildTime: "2026-07-25T01:33:41Z",
want: "vintner 0.3.0 (6186837fd236, 2026-07-25T01:33:41Z)",
},
{
name: "dirty tree",
ver: "0.3.0",
revision: "6186837fd23616335ba8aff830801692a756799c",
dirty: true,
want: "vintner 0.3.0 (6186837fd236-dirty)",
},
{
name: "short revision left untouched",
ver: "dev",
revision: "abc123",
want: "vintner dev (abc123)",
},
} {
t.Run(tc.name, func(t *testing.T) {
got := formatVersion(tc.ver, tc.revision, tc.buildTime, tc.dirty)
if got != tc.want {
t.Errorf("formatVersion(%q, %q, %q, %v) = %q, want %q",
tc.ver, tc.revision, tc.buildTime, tc.dirty, got, tc.want)
}
})
}
}