Repository: kopia/kopia
Pull request #4498 (2025-04-12 19:56:12)

build(cli): set build info version and info when not set
Sets `repo.BuildInfo` and `repo.BuildVersion` when they are not specified via link flags.

The behavior for binaries built via CI and Make remains the same.

codecov[bot]: ## [Codecov](https://app.codecov.io/gh/kopia/kopia/pull/4498?dropdown=coverage&src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=kopia) Report
Attention: Patch coverage is `78.00000%` with `11 lines` in your changes missing coverage. Please review.
> Project coverage is 76.32%. Comparing base [(`cb455c6`)](https://app.codecov.io/gh/kopia/kopia/commit/cb455c699a04e54cb47e25a676d7d65063ce883e?dropdown=coverage&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=kopia) to head [(`978d451`)](https://app.codecov.io/gh/kopia/kopia/commit/978d451ecf608f434d5db7bf25ef477b30685c46?dropdown=coverage&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=kopia).
> Report is 462 commits behind head on master.

| [Files with missing lines](https://app.codecov.io/gh/kopia/kopia/pull/4498?dropdown=coverage&src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=kopia) | Patch % | Lines |
|---|---|---|
| [repo/buildinfo.go](https://app.codecov.io/gh/kopia/kopia/pull/4498?src=pr&el=tree&filepath=repo%2Fbuildinfo.go&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=kopia#diff-cmVwby9idWlsZGluZm8uZ28=) | 78.00% | [8 Missing and 3 partials :warning: ](https://app.codecov.io/gh/kopia/kopia/pull/4498?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=kopia) |

<details><summary>Additional details and impacted files</summary>


```diff
@@            Coverage Diff             @@
##           master    #4498      +/-   ##
==========================================
+ Coverage   75.86%   76.32%   +0.45%     
==========================================
  Files         470      528      +58     
  Lines       37301    40050    +2749     
==========================================
+ Hits        28299    30567    +2268     
- Misses       7071     7472     +401     
- Partials     1931     2011      +80     
```

</details>

[:umbrella: View full report in Codecov by Sentry](https://app.codecov.io/gh/kopia/kopia/pull/4498?dropdown=coverage&src=pr&el=continue&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=kopia).   
:loudspeaker: Have feedback on the report? [Share it here](https://about.codecov.io/codecov-pr-comment-feedback/?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=kopia).

<details><summary> :rocket: New features to boost your workflow: </summary>

- :snowflake: [Test Analytics](https://docs.codecov.com/docs/test-analytics): Detect flaky tests, report on failures, and find test suite problems.
- :package: [JS Bundle Analysis](https://docs.codecov.com/docs/javascript-bundle-analysis): Save yourself from yourself by tracking and limiting bundle sizes in JS merges.
</details>

Patch:
diff --git a/repo/buildinfo.go b/repo/buildinfo.go
new file mode 100644
index 00000000..c6f7a054
--- /dev/null
+++ b/repo/buildinfo.go
@@ -0,0 +1,84 @@
+package repo
+
+import (
+	stdlib "log"
+	"runtime/debug"
+	"strings"
+)
+
+// Kopia's build information.
+//
+//nolint:gochecknoglobals
+var (
+	BuildInfo       = ""
+	BuildVersion    = ""
+	BuildGitHubRepo = ""
+)
+
+func init() {
+	// fill in from executable's build info when these are unset
+	BuildInfo, BuildVersion = getBuildInfoAndVersion(BuildInfo, BuildVersion)
+}
+
+func getBuildInfoAndVersion(linkedInfo, linkedVersion string) (info, version string) {
+	info, version = linkedInfo, linkedVersion
+
+	if info != "" && version != "" {
+		return // use the values specified at link time
+	}
+
+	// a value was not set at link time, set it from the executable's build
+	// info if available.
+	bi, ok := debug.ReadBuildInfo()
+	if !ok {
+		// logging not yet set up, use stdlib's logging
+		stdlib.Println("executable build information is not available")
+		return // executable's build info is not available, use values set at link time, if any
+	}
+
+	if version == "" {
+		version = "v0-unofficial"
+
+		if bi.Main.Version != "" && bi.Main.Version != "(devel)" { // set to '(devel)' during tests in Go 1.24
+			version = bi.Main.Version
+		}
+	}
+
+	if info == "" {
+		info = getRevisionString(bi.Settings)
+	}
+
+	return
+}
+
+func getRevisionString(s []debug.BuildSetting) string {
+	var (
+		revision, vcsTime string
+		modified          bool
+	)
+
+	for _, v := range s {
+		switch v.Key {
+		case "vcs.revision":
+			revision = v.Value
+		case "vcs.time":
+			vcsTime = v.Value
+		case "vcs.modified":
+			if strings.EqualFold(v.Value, "true") {
+				modified = true
+			}
+		}
+	}
+
+	if revision == "" {
+		revision = "(unknown_revision)"
+	}
+
+	var modStr string
+
+	if modified {
+		modStr = "+dirty"
+	}
+
+	return vcsTime + "-" + revision + modStr
+}
diff --git a/repo/initialize.go b/repo/initialize.go
index 7b9e7a77..ce1c931d 100644
--- a/repo/initialize.go
+++ b/repo/initialize.go
@@ -18,15 +18,6 @@
 	"github.com/kopia/kopia/repo/splitter"
 )
 
-// BuildInfo is the build information of Kopia.
-//
-//nolint:gochecknoglobals
-var (
-	BuildInfo       = "unknown"
-	BuildVersion    = "v0-unofficial"
-	BuildGitHubRepo = ""
-)
-
 const (
 	hmacSecretLength = 32
 	masterKeyLength  = 32


Test patch:
diff --git a/repo/buildinfo_test.go b/repo/buildinfo_test.go
new file mode 100644
index 00000000..5164ff7f
--- /dev/null
+++ b/repo/buildinfo_test.go
@@ -0,0 +1,113 @@
+package repo
+
+import (
+	"runtime/debug"
+	"testing"
+
+	"github.com/stretchr/testify/require"
+)
+
+func TestGetRevisionString(t *testing.T) {
+	cases := []struct {
+		input []debug.BuildSetting
+		want  string
+	}{
+		{
+			want: "-(unknown_revision)",
+		},
+		{
+			input: []debug.BuildSetting{
+				{
+					Key:   "vcs.modified",
+					Value: "true",
+				},
+			},
+			want: "-(unknown_revision)+dirty",
+		},
+		{
+			input: []debug.BuildSetting{
+				{
+					Key:   "vcs.time",
+					Value: "2025-04-12T16:01:30Z",
+				},
+			},
+			want: "2025-04-12T16:01:30Z-(unknown_revision)",
+		},
+		{
+			input: []debug.BuildSetting{
+				{
+					Key:   "vcs.time",
+					Value: "2025-04-12T16:01:30Z",
+				},
+				{
+					Key:   "vcs.modified",
+					Value: "true",
+				},
+			},
+			want: "2025-04-12T16:01:30Z-(unknown_revision)+dirty",
+		},
+		{
+			input: []debug.BuildSetting{
+				{
+					Key:   "vcs.time",
+					Value: "2025-04-12T16:01:30Z",
+				},
+				{
+					Key:   "vcs.revision",
+					Value: "353676da445938316fa00b2b812a61f4b1dd3ffa",
+				},
+			},
+			want: "2025-04-12T16:01:30Z-353676da445938316fa00b2b812a61f4b1dd3ffa",
+		},
+		{
+			input: []debug.BuildSetting{
+				{
+					Key:   "vcs.time",
+					Value: "2025-04-12T16:01:30Z",
+				},
+				{
+					Key:   "vcs.revision",
+					Value: "353676da4459",
+				},
+			},
+			want: "2025-04-12T16:01:30Z-353676da4459",
+		},
+		{
+			input: []debug.BuildSetting{
+				{
+					Key:   "vcs.time",
+					Value: "2025-04-12T16:01:30Z",
+				},
+				{
+					Key:   "vcs.revision",
+					Value: "353676da",
+				},
+			},
+			want: "2025-04-12T16:01:30Z-353676da",
+		},
+		{
+			input: []debug.BuildSetting{
+				{
+					Key:   "vcs.time",
+					Value: "2025-04-12T16:01:30Z",
+				},
+				{
+					Key:   "vcs.revision",
+					Value: "353676da445938316fa00b2b812a61f4b1dd3ffa",
+				},
+				{
+					Key:   "vcs.modified",
+					Value: "true",
+				},
+			},
+			want: "2025-04-12T16:01:30Z-353676da445938316fa00b2b812a61f4b1dd3ffa+dirty",
+		},
+	}
+
+	for _, c := range cases {
+		t.Run("buildinfo", func(t *testing.T) {
+			got := getRevisionString(c.input)
+			require.Equal(t, c.want, got)
+		})
+	}
+}