diff --git a/internal/cli/version.go b/internal/cli/version.go index 779feb7..f98c02c 100644 --- a/internal/cli/version.go +++ b/internal/cli/version.go @@ -2,13 +2,15 @@ package cli import ( "fmt" + "runtime/debug" + "strings" "github.com/spf13/cobra" ) // version, commit, and date are set at build time via -ldflags (see // .goreleaser.yaml). They default to values that make an unflagged build -// identify itself as a dev build. +// fall back to the build information Go embeds in every binary. var ( version = "dev" commit = "" @@ -29,11 +31,60 @@ func newVersionCmd() *cobra.Command { } } -// versionString formats the version, commit, and date set by ldflags. When -// none of them were set, it reports "cdd dev (dev)". +// versionString formats the version from ldflags when GoReleaser set them, +// otherwise from the build information Go embeds: the module version for +// a `go install module@tag` build, the VCS revision for a build from a +// checkout, and "cdd dev (dev)" when neither is known. func versionString() string { - if version == "dev" && commit == "" && date == "" { + info, _ := debug.ReadBuildInfo() + return formatVersion(version, commit, date, info) +} + +// formatVersion is the pure core of versionString, split out so tests can +// feed it ldflags values and build information directly. +func formatVersion(version, commit, date string, info *debug.BuildInfo) string { + if version != "dev" || commit != "" || date != "" { + return fmt.Sprintf("cdd %s (%s %s)", version, commit, date) + } + if info == nil { return "cdd dev (dev)" } - return fmt.Sprintf("cdd %s (%s %s)", version, commit, date) + + if v := info.Main.Version; v != "" && v != "(devel)" { + return fmt.Sprintf("cdd %s (go install)", strings.TrimPrefix(v, "v")) + } + + rev, when, modified := vcsSettings(info) + if rev == "" { + return "cdd dev (dev)" + } + if modified { + rev += "+dirty" + } + if when == "" { + return fmt.Sprintf("cdd dev (%s)", rev) + } + return fmt.Sprintf("cdd dev (%s %s)", rev, when) +} + +// vcsSettings extracts the short revision, commit date, and dirty flag that +// Go records when building from a version-controlled checkout. +func vcsSettings(info *debug.BuildInfo) (rev, when string, modified bool) { + for _, s := range info.Settings { + switch s.Key { + case "vcs.revision": + rev = s.Value + if len(rev) > 7 { + rev = rev[:7] + } + case "vcs.time": + when = s.Value + if len(when) > 10 { + when = when[:10] + } + case "vcs.modified": + modified = s.Value == "true" + } + } + return rev, when, modified } diff --git a/internal/cli/version_test.go b/internal/cli/version_test.go new file mode 100644 index 0000000..ed19008 --- /dev/null +++ b/internal/cli/version_test.go @@ -0,0 +1,47 @@ +package cli + +import ( + "runtime/debug" + "testing" +) + +func TestFormatVersion(t *testing.T) { + vcs := &debug.BuildInfo{ + Main: debug.Module{Version: "(devel)"}, + Settings: []debug.BuildSetting{ + {Key: "vcs.revision", Value: "b72f0117d2c4a9e1f0c3"}, + {Key: "vcs.time", Value: "2026-09-17T07:20:28Z"}, + {Key: "vcs.modified", Value: "false"}, + }, + } + dirty := &debug.BuildInfo{ + Main: debug.Module{Version: "(devel)"}, + Settings: []debug.BuildSetting{ + {Key: "vcs.revision", Value: "b72f011"}, + {Key: "vcs.modified", Value: "true"}, + }, + } + + tests := []struct { + name string + version, commit, date string + info *debug.BuildInfo + want string + }{ + {name: "ldflags from GoReleaser", version: "0.1.1", commit: "b72f011", date: "2026-09-17", info: vcs, want: "cdd 0.1.1 (b72f011 2026-09-17)"}, + {name: "go install of a tagged module", version: "dev", info: &debug.BuildInfo{Main: debug.Module{Version: "v0.1.1"}}, want: "cdd 0.1.1 (go install)"}, + {name: "build from a clean checkout", version: "dev", info: vcs, want: "cdd dev (b72f011 2026-09-17)"}, + {name: "build from a dirty checkout", version: "dev", info: dirty, want: "cdd dev (b72f011+dirty)"}, + {name: "no build information", version: "dev", info: nil, want: "cdd dev (dev)"}, + {name: "devel without vcs", version: "dev", info: &debug.BuildInfo{Main: debug.Module{Version: "(devel)"}}, want: "cdd dev (dev)"}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got := formatVersion(tt.version, tt.commit, tt.date, tt.info) + if got != tt.want { + t.Errorf("formatVersion() = %q, want %q", got, tt.want) + } + }) + } +}