From e49c93d403afd662eeb3579f9a42ac45e75b7bef Mon Sep 17 00:00:00 2001 From: sudoforge Date: Fri, 23 May 2025 10:07:38 -0700 Subject: [PATCH] build: reduce complexity for setting the version (#1466) This change refactors the implementation of how the version is embedded in the binary to reduce the number of variables necessary to determine the version information from 3 to 1. The legacy build variables are still supported, however, a warning will be emitted instructing users to contact their package maintainer. The legacy GitExacTag variable, if present, will be used to set main.version if it is undefined. This ensures that unmigrated package builds will continue to provide the correct version information. The legacy build variables will be supported until 0.12.0, giving package maintainers some time to migrate. Change-Id: I05fea97169ea1af87b198174afe5b6663f860fd8 --- Makefile | 13 ++--- commands/root.go | 79 ++----------------------------- commands/version.go | 87 +++++++++++++++++----------------- doc/generate.go | 2 +- doc/man/git-bug-version.1 | 37 +++++++++++---- doc/md/git-bug.md | 2 +- doc/md/git-bug_version.md | 38 +++++++++++++-- main.go | 8 +++- misc/completion/generate.go | 2 +- version.go | 94 +++++++++++++++++++++++++++++++++++++ 10 files changed, 219 insertions(+), 143 deletions(-) create mode 100644 version.go diff --git a/Makefile b/Makefile index 6086799265e42ec59ffe1fd5df07449936162700..4745d195db612ace950380c2760d0fd3d4217a89 100644 --- a/Makefile +++ b/Makefile @@ -1,8 +1,3 @@ -all: build - -GIT_COMMIT:=$(shell git rev-list -1 HEAD) -GIT_LAST_TAG:=$(shell git describe --abbrev=0 --tags 2>/dev/null || true) -GIT_EXACT_TAG:=$(shell git name-rev --name-only --tags HEAD) UNAME_S := $(shell uname -s) XARGS:=xargs -r ifeq ($(UNAME_S),Darwin) @@ -11,10 +6,10 @@ endif SYSTEM=$(shell nix eval --impure --expr 'builtins.currentSystem' --raw 2>/dev/null || echo '') -COMMANDS_PATH:=github.com/git-bug/git-bug/commands -LDFLAGS:=-X ${COMMANDS_PATH}.GitCommit="${GIT_COMMIT}" \ - -X ${COMMANDS_PATH}.GitLastTag="${GIT_LAST_TAG}" \ - -X ${COMMANDS_PATH}.GitExactTag="${GIT_EXACT_TAG}" +TAG:=$(shell git name-rev --name-only --tags HEAD) +LDFLAGS:=-X main.version="${TAG}" + +all: build .PHONY: list-checks list-checks: diff --git a/commands/root.go b/commands/root.go index 1a4109a3cc6e057c92d9385258fcf75d1137d2c6..1b64b5090b624f29c923285667ebe7b2d8924174 100644 --- a/commands/root.go +++ b/commands/root.go @@ -1,25 +1,17 @@ -// Package commands contains the CLI commands package commands import ( - "fmt" "os" - "runtime/debug" "github.com/spf13/cobra" - bridgecmd "github.com/git-bug/git-bug/commands/bridge" - bugcmd "github.com/git-bug/git-bug/commands/bug" + "github.com/git-bug/git-bug/commands/bridge" + "github.com/git-bug/git-bug/commands/bug" "github.com/git-bug/git-bug/commands/execenv" - usercmd "github.com/git-bug/git-bug/commands/user" + "github.com/git-bug/git-bug/commands/user" ) -// These variables are initialized externally during the build. See the Makefile. -var GitCommit string -var GitLastTag string -var GitExactTag string - -func NewRootCommand() *cobra.Command { +func NewRootCommand(version string) *cobra.Command { cmd := &cobra.Command{ Use: execenv.RootCommandName, Short: "A bug tracker embedded in Git", @@ -33,7 +25,7 @@ the same git remote you are already using to collaborate with other people. PersistentPreRun: func(cmd *cobra.Command, args []string) { root := cmd.Root() - root.Version = getVersion() + root.Version = version }, // For the root command, force the execution of the PreRun @@ -80,64 +72,3 @@ the same git remote you are already using to collaborate with other people. return cmd } - -func Execute() { - if err := NewRootCommand().Execute(); err != nil { - os.Exit(1) - } -} - -func getVersion() string { - if GitExactTag == "undefined" { - GitExactTag = "" - } - - if GitExactTag != "" { - // we are exactly on a tag --> release version - return GitLastTag - } - - if GitLastTag != "" { - // not exactly on a tag --> dev version - return fmt.Sprintf("%s-dev-%.10s", GitLastTag, GitCommit) - } - - // we don't have commit information, try golang build info - if commit, dirty, err := getCommitAndDirty(); err == nil { - if dirty { - return fmt.Sprintf("dev-%.10s-dirty", commit) - } - return fmt.Sprintf("dev-%.10s", commit) - } - - return "dev-unknown" -} - -func getCommitAndDirty() (commit string, dirty bool, err error) { - info, ok := debug.ReadBuildInfo() - if !ok { - return "", false, fmt.Errorf("unable to read build info") - } - - var commitFound bool - - // get the commit and modified status - // (that is the flag for repository dirty or not) - for _, kv := range info.Settings { - switch kv.Key { - case "vcs.revision": - commit = kv.Value - commitFound = true - case "vcs.modified": - if kv.Value == "true" { - dirty = true - } - } - } - - if !commitFound { - return "", false, fmt.Errorf("no commit found") - } - - return commit, dirty, nil -} diff --git a/commands/version.go b/commands/version.go index 189a2e350f563be8432e9c275544476fb0c143a5..ffa4a180afd47e7a7227a713972241284a1ce745 100644 --- a/commands/version.go +++ b/commands/version.go @@ -1,63 +1,66 @@ package commands import ( - "runtime" + "log/slog" "github.com/spf13/cobra" "github.com/git-bug/git-bug/commands/execenv" ) -type versionOptions struct { - number bool - commit bool - all bool -} +// TODO: 0.12.0: remove deprecated build vars +var ( + GitCommit string + GitLastTag string + GitExactTag string +) func newVersionCommand(env *execenv.Env) *cobra.Command { - options := versionOptions{} + return &cobra.Command{ + Use: "version", + Short: "Print version information", + Example: "git bug version", + Long: ` +Print version information. - cmd := &cobra.Command{ - Use: "version", - Short: "Show git-bug version information", - Run: func(cmd *cobra.Command, args []string) { - runVersion(env, options, cmd.Root()) - }, - } +Format: + git-bug [commit[/dirty]] - flags := cmd.Flags() - flags.SortFlags = false +Format Description: + may be one of: + - A semantic version string, prefixed with a "v", e.g. v1.2.3 + - "undefined" (if not provided, or built with an invalid version string) - flags.BoolVarP(&options.number, "number", "n", false, - "Only show the version number", - ) - flags.BoolVarP(&options.commit, "commit", "c", false, - "Only show the commit hash", - ) - flags.BoolVarP(&options.all, "all", "a", false, - "Show all version information", - ) + [commit], if present, is the commit hash that was checked out during the + build. This may be suffixed with '/dirty' if there were local file + modifications. This is indicative of your build being patched, or modified in + some way from the commit. - return cmd -} + is the version of the go compiler used for the build. -func runVersion(env *execenv.Env, opts versionOptions, root *cobra.Command) { - if opts.all { - env.Out.Printf("%s version: %s\n", execenv.RootCommandName, root.Version) - env.Out.Printf("System version: %s/%s\n", runtime.GOARCH, runtime.GOOS) - env.Out.Printf("Golang version: %s\n", runtime.Version()) - return - } + is the target platform (GOOS). - if opts.number { - env.Out.Println(root.Version) - return + is the target architecture (GOARCH). +`, + Run: func(cmd *cobra.Command, args []string) { + defer warnDeprecated() + env.Out.Printf("%s %s", execenv.RootCommandName, cmd.Root().Version) + }, } +} - if opts.commit { - env.Out.Println(GitCommit) - return +// warnDeprecated warns about deprecated build variables +// TODO: 0.12.0: remove support for old build tags +func warnDeprecated() { + msg := "please contact your package maintainer" + reason := "deprecated build variable" + if GitLastTag != "" { + slog.Warn(msg, "reason", reason, "name", "GitLastTag", "value", GitLastTag) + } + if GitExactTag != "" { + slog.Warn(msg, "reason", reason, "name", "GitExactTag", "value", GitExactTag) + } + if GitCommit != "" { + slog.Warn(msg, "reason", reason, "name", "GitCommit", "value", GitCommit) } - - env.Out.Printf("%s version: %s\n", execenv.RootCommandName, root.Version) } diff --git a/doc/generate.go b/doc/generate.go index 0001f1a77358010f53c98578fd49e088f16e0d97..005d9df7627e064ae26527d3b3381a0c01a0f471 100644 --- a/doc/generate.go +++ b/doc/generate.go @@ -34,7 +34,7 @@ func main() { wg.Add(1) go func(name string, f func(*cobra.Command) error) { defer wg.Done() - root := commands.NewRootCommand() + root := commands.NewRootCommand("") err := f(root) if err != nil { fmt.Printf(" - %s: FATAL\n", name) diff --git a/doc/man/git-bug-version.1 b/doc/man/git-bug-version.1 index 7183b696e17be709632732b5dcc5b4956b74836c..988849e8afc14998940f467c2b9083bbaecd0f73 100644 --- a/doc/man/git-bug-version.1 +++ b/doc/man/git-bug-version.1 @@ -2,7 +2,7 @@ .TH "GIT-BUG" "1" "Apr 2019" "Generated from git-bug's source code" "" .SH NAME -git-bug-version - Show git-bug version information +git-bug-version - Print version information .SH SYNOPSIS @@ -10,25 +10,44 @@ git-bug-version - Show git-bug version information .SH DESCRIPTION -Show git-bug version information +Print version information. +.PP +Format: + git-bug [commit[/dirty]] -.SH OPTIONS -\fB-n\fP, \fB--number\fP[=false] - Only show the version number +.PP +Format Description: + may be one of: + - A semantic version string, prefixed with a "v", e.g. v1.2.3 + - "undefined" (if not provided, or built with an invalid version string) + +.PP +[commit], if present, is the commit hash that was checked out during the + build. This may be suffixed with '/dirty' if there were local file + modifications. This is indicative of your build being patched, or modified in + some way from the commit. .PP -\fB-c\fP, \fB--commit\fP[=false] - Only show the commit hash + is the version of the go compiler used for the build. .PP -\fB-a\fP, \fB--all\fP[=false] - Show all version information + is the target platform (GOOS). .PP + is the target architecture (GOARCH). + + +.SH OPTIONS \fB-h\fP, \fB--help\fP[=false] help for version +.SH EXAMPLE +.EX +git bug version +.EE + + .SH SEE ALSO \fBgit-bug(1)\fP diff --git a/doc/md/git-bug.md b/doc/md/git-bug.md index 03bebb65e7ae1588df7a93077a58f3822d5164bc..2ef0b77252bd5f7bc7ca4835a002688a51d871a6 100644 --- a/doc/md/git-bug.md +++ b/doc/md/git-bug.md @@ -31,7 +31,7 @@ git-bug [flags] * [git-bug push](git-bug_push.md) - Push updates to a git remote * [git-bug termui](git-bug_termui.md) - Launch the terminal UI * [git-bug user](git-bug_user.md) - List identities -* [git-bug version](git-bug_version.md) - Show git-bug version information +* [git-bug version](git-bug_version.md) - Print version information * [git-bug webui](git-bug_webui.md) - Launch the web UI * [git-bug wipe](git-bug_wipe.md) - Wipe git-bug from the git repository diff --git a/doc/md/git-bug_version.md b/doc/md/git-bug_version.md index ceba8790ff8d1dcbda7184f6803d98cba5f4f42e..a2569aff160794b6b145f62397804c52ebec5412 100644 --- a/doc/md/git-bug_version.md +++ b/doc/md/git-bug_version.md @@ -1,18 +1,46 @@ ## git-bug version -Show git-bug version information +Print version information + +### Synopsis + + +Print version information. + +Format: + git-bug [commit[/dirty]] + +Format Description: + may be one of: + - A semantic version string, prefixed with a "v", e.g. v1.2.3 + - "undefined" (if not provided, or built with an invalid version string) + + [commit], if present, is the commit hash that was checked out during the + build. This may be suffixed with '/dirty' if there were local file + modifications. This is indicative of your build being patched, or modified in + some way from the commit. + + is the version of the go compiler used for the build. + + is the target platform (GOOS). + + is the target architecture (GOARCH). + ``` git-bug version [flags] ``` +### Examples + +``` +git bug version +``` + ### Options ``` - -n, --number Only show the version number - -c, --commit Only show the commit hash - -a, --all Show all version information - -h, --help help for version + -h, --help help for version ``` ### SEE ALSO diff --git a/main.go b/main.go index 7a2034baba99d4e9edff6c5b36e4ff683fc26e03..5b7a4caa6343152182bfd32492c48987ce71ad32 100644 --- a/main.go +++ b/main.go @@ -4,9 +4,15 @@ package main import ( + "os" + "github.com/git-bug/git-bug/commands" ) func main() { - commands.Execute() + v, _ := getVersion() + root := commands.NewRootCommand(v) + if err := root.Execute(); err != nil { + os.Exit(1) + } } diff --git a/misc/completion/generate.go b/misc/completion/generate.go index b64c1034f672e3a4885f33ae9f8673809252e9bb..5c64681214fe88320119c2e36509734d762fa72b 100644 --- a/misc/completion/generate.go +++ b/misc/completion/generate.go @@ -26,7 +26,7 @@ func main() { wg.Add(1) go func(name string, f func(*cobra.Command) error) { defer wg.Done() - root := commands.NewRootCommand() + root := commands.NewRootCommand("") err := f(root) if err != nil { fmt.Printf(" - %s: %v\n", name, err) diff --git a/version.go b/version.go new file mode 100644 index 0000000000000000000000000000000000000000..de9bd8753b6d0b82a803e9031fc96167b3ec066c --- /dev/null +++ b/version.go @@ -0,0 +1,94 @@ +package main + +import ( + "errors" + "fmt" + "runtime/debug" + "strings" + + "github.com/git-bug/git-bug/commands" + "golang.org/x/mod/semver" +) + +var ( + version = "undefined" +) + +// getVersion returns a string representing the version information defined when +// the binary was built, or a sane default indicating a local build. a string is +// always returned. an error may be returned along with the string in the event +// that we detect a local build but are unable to get build metadata. +// +// TODO: support validation of the version (that it's a real version) +// TODO: support notifying the user if their version is out of date +func getVersion() (string, error) { + var arch string + var commit string + var modified bool + var platform string + + var v strings.Builder + + // this supports overriding the default version if the deprecated var used + // for setting the exact version for releases is supplied. we are doing this + // in order to give downstream package maintainers a longer window to + // migrate. + // + // TODO: 0.12.0: remove support for old build tags + if version == "undefined" && commands.GitExactTag != "" { + version = commands.GitExactTag + } + + // automatically add the v prefix if it's missing + if version != "undefined" && !strings.HasPrefix(version, "v") { + version = fmt.Sprintf("v%s", version) + } + + // reset the version string to undefined if it is invalid + if ok := semver.IsValid(version); !ok { + version = "undefined" + } + + v.WriteString(version) + + info, ok := debug.ReadBuildInfo() + if !ok { + v.WriteString(fmt.Sprintf(" (no build info)\n")) + return v.String(), errors.New("unable to read build metadata") + } + + for _, kv := range info.Settings { + switch kv.Key { + case "GOOS": + platform = kv.Value + case "GOARCH": + arch = kv.Value + case "vcs.modified": + if kv.Value == "true" { + modified = true + } + case "vcs.revision": + commit = kv.Value + } + } + + if commit != "" { + v.WriteString(fmt.Sprintf(" %.12s", commit)) + } + + if modified { + v.WriteString("/dirty") + } + + v.WriteString(fmt.Sprintf(" %s", info.GoVersion)) + + if platform != "" { + v.WriteString(fmt.Sprintf(" %s", platform)) + } + + if arch != "" { + v.WriteString(fmt.Sprintf(" %s", arch)) + } + + return fmt.Sprint(v.String(), "\n"), nil +}