From 94a19e6b433173c8ca2c52e0bf4c8c883e7ba4ad Mon Sep 17 00:00:00 2001 From: Cheviiot <153805936+Cheviiot@users.noreply.github.com> Date: Sat, 25 Jul 2026 11:37:02 +1000 Subject: [PATCH] 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. --- cmd/vintner/main.go | 2 +- cmd/vintner/version.go | 56 +++++++++++++++++++++++++++++++++++++ cmd/vintner/version_test.go | 46 ++++++++++++++++++++++++++++++ 3 files changed, 103 insertions(+), 1 deletion(-) create mode 100644 cmd/vintner/version.go create mode 100644 cmd/vintner/version_test.go diff --git a/cmd/vintner/main.go b/cmd/vintner/main.go index 1541eae..4c264b8 100644 --- a/cmd/vintner/main.go +++ b/cmd/vintner/main.go @@ -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() diff --git a/cmd/vintner/version.go b/cmd/vintner/version.go new file mode 100644 index 0000000..e9d8efd --- /dev/null +++ b/cmd/vintner/version.go @@ -0,0 +1,56 @@ +package main + +import ( + "fmt" + "runtime/debug" +) + +// versionString renders "vintner " 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) +} diff --git a/cmd/vintner/version_test.go b/cmd/vintner/version_test.go new file mode 100644 index 0000000..27d2d97 --- /dev/null +++ b/cmd/vintner/version_test.go @@ -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) + } + }) + } +}