diff --git a/CHANGELOG.md b/CHANGELOG.md index 8350f2e..4519acf 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -88,9 +88,13 @@ All notable changes to Jarl are documented here. The format follows linking to the release on Gitea. A build that isn't of a release tag shows its commit instead, linked to the commit and marked `-modified` with uncommitted changes. Before, every build of a commit after a - release still showed the release's version. `jarld version`, - `jarl --version`, `GET /status` and `/auth/whoami` report the same, the - latter two as `build` (docs/development.md). + release still showed the release's version. `GET /status` and + `/auth/whoami` report the build as `build` (docs/development.md). +- `jarld version`, `jarl --version`, `jarl status` and `jarl whoami` give + a version number only for release builds. Other builds say they are + development builds and how far past the last tag they are: `dev + ca0f148b78b6 (3 commits after v0.2.0-rc.1)`. In 0.2.0-rc.1 they still + printed `0.2.0-rc.1 (ca0f148b78b6)`. - The view in the zone list, and on the zone page, links to that view, and the views page highlights it. It used to be plain text, and the zone page linked to the views page as a whole. diff --git a/Makefile b/Makefile index 51d345b..503a921 100644 --- a/Makefile +++ b/Makefile @@ -18,7 +18,11 @@ VERSION ?= $(shell cat VERSION 2>/dev/null || echo 0.0.0-dev) # the release rather than the commit): set when HEAD is tagged v$(VERSION) # and no tracked file is changed. RELEASE_TAG ?= $(shell t=v$(VERSION); git tag --points-at HEAD 2>/dev/null | grep -qx "$$t" && git diff --quiet HEAD 2>/dev/null && echo $$t) -LDFLAGS := -s -w -X jarl/internal/version.Version=$(VERSION) -X jarl/internal/version.Tag=$(RELEASE_TAG) +# DESCRIBE places other builds relative to the last tag ("3 commits after +# v0.2.0-rc.1"). +DESCRIBE ?= $(shell git describe --tags --long --match 'v[0-9]*' --abbrev=12 HEAD 2>/dev/null) +LDFLAGS := -s -w -X jarl/internal/version.Version=$(VERSION) -X jarl/internal/version.Tag=$(RELEASE_TAG) \ + -X jarl/internal/version.Describe=$(DESCRIBE) GOFLAGS := -trimpath export CGO_ENABLED := 0 diff --git a/docs/development.md b/docs/development.md index 9dc266d..69c2a66 100644 --- a/docs/development.md +++ b/docs/development.md @@ -232,17 +232,23 @@ runs it as a check before building. ### Which version a build reports -The footer of the web UI, `jarld version` and `jarl --version` say what a -binary was built from, linking to it on Gitea: +The footer of the web UI, `jarld version`, `jarl --version`, `jarl status` +and `jarl whoami` say what a binary was built from (the footer links to +it on Gitea). Only a release build reports a version number: * **A release**: `make` run on the commit tagged `v` with no tracked file changed. The Makefile then sets `RELEASE_TAG`, and the footer reads "Powered by Jarl v0.2.0", linking to the release. The release workflow sets `RELEASE_TAG` itself. -* **Any other build**: the commit, which Go records from git: "Powered by - Jarl 1a2b3c4d5e6f", linking to the commit. With uncommitted changes it - is marked `-modified`. The version commands print `0.2.0 (1a2b3c4d5e6f)`. -* **No git** (a build from a source tarball): just `VERSION`. +* **Any other build** is a development build, named by its commit (which + Go records from git) rather than `VERSION`: "Powered by Jarl + 1a2b3c4d5e6f", linking to the commit. With uncommitted changes it is + marked `-modified`. The other places add where it stands relative to + the last tag, from `git describe` (the Makefile's `DESCRIBE`): + `dev 1a2b3c4d5e6f (3 commits after v0.2.0-rc.1)`, or + `dev 1a2b3c4d5e6f-modified (v0.2.0-rc.1 with uncommitted changes)`. +* **No git** (a build from a source archive): `VERSION`, marked as such: + `0.2.0 (source build)`. `RELEASE_TAG=v0.2.0 make` forces the first case. diff --git a/internal/client/client.go b/internal/client/client.go index a3495bf..b226b12 100644 --- a/internal/client/client.go +++ b/internal/client/client.go @@ -183,7 +183,7 @@ func (c *Client) Do(ctx context.Context, method, path string, query url.Values, req.Header.Set("Content-Type", "application/json") } req.Header.Set("Accept", "application/json") - req.Header.Set("User-Agent", "jarl-cli/"+version.Version) + req.Header.Set("User-Agent", "jarl-cli/"+version.Short()) if c.Token != "" { req.Header.Set("Authorization", "Bearer "+c.Token) } diff --git a/internal/version/version.go b/internal/version/version.go index 8208b4b..f972054 100644 --- a/internal/version/version.go +++ b/internal/version/version.go @@ -4,12 +4,17 @@ // // The Makefile sets Version from the VERSION file, and Tag only when the // build is of that release: HEAD is the tag v and no tracked file -// is changed. Other builds are identified by their commit, which Go -// records from git by itself. +// is changed. Only such a build reports a version number. Other builds +// are development builds, identified by their commit (which Go records +// from git by itself) and by where they stand relative to the last tag +// (Describe, from git describe). package version import ( + "regexp" "runtime/debug" + "strconv" + "strings" "sync" ) @@ -23,6 +28,11 @@ var Tag = "" // information when not set at build time. var Commit = "" +// Describe is 'git describe --tags --long' of the build's commit +// ("v0.2.0-rc.1-3-gca0f148b78b6"), set by the Makefile; it says how many +// commits a development build is past the last tag. +var Describe = "" + // Repo is Jarl's source repository. const Repo = "https://gitea.adbraun.lol/adam/jarl" @@ -32,6 +42,11 @@ type Build struct { Tag string `json:"tag,omitempty"` Commit string `json:"commit,omitempty"` Modified bool `json:"modified,omitempty"` // built with uncommitted changes + // Since is the last tag before a development build's commit, and + // Ahead how many commits the build is past it (0: the tag's commit, + // with uncommitted changes). + Since string `json:"since,omitempty"` + Ahead int `json:"ahead,omitempty"` // Label is the short form to show: the tag of a release, otherwise // the abbreviated commit (or the version when the commit is unknown). Label string `json:"label"` @@ -53,9 +68,23 @@ var info = sync.OnceValue(func() Build { } } } - return describe(Version, Tag, commit, modified) + b := describe(Version, Tag, commit, modified) + b.Since, b.Ahead = parseDescribe(Describe, b.Commit) + return b }) +// parseDescribe splits git describe --long output, if it is of commit. +func parseDescribe(d, commit string) (since string, ahead int) { + m := describeRe.FindStringSubmatch(d) + if m == nil || commit == "" || !strings.HasPrefix(commit, m[3]) { + return "", 0 + } + n, _ := strconv.Atoi(m[2]) + return m[1], n +} + +var describeRe = regexp.MustCompile(`^(v.+)-(\d+)-g([0-9a-f]+)$`) + func describe(version, tag, commit string, modified bool) Build { b := Build{Version: version, Tag: tag, Commit: commit, Modified: modified} switch { @@ -76,14 +105,41 @@ func describe(version, tag, commit string, modified bool) Build { // Info describes this build. func Info() Build { return info() } -// String is the version for logs and 'version' commands: "0.2.0" for a -// release, otherwise the version and the commit ("0.2.0-dev (a1b2c3d4e5f6)"). -func String() string { - b := Info() - if b.Tag != "" || b.Commit == "" { +// String is the version for logs and 'version' commands. Only a release +// build gives a version ("0.2.0"); any other build says it's a +// development build: "dev a1b2c3d4e5f6 (3 commits after v0.2.0-rc.1)". +func String() string { return Info().String() } + +func (b Build) String() string { + switch { + case b.Tag != "": return b.Version + case b.Commit == "": + // No git information (built from a source archive). + return b.Version + " (source build)" } - return b.Version + " (" + b.Label + ")" + s := "dev " + b.Label + switch { + case b.Since == "": + case b.Ahead == 1: + s += " (1 commit after " + b.Since + ")" + case b.Ahead > 1: + s += " (" + strconv.Itoa(b.Ahead) + " commits after " + b.Since + ")" + case b.Modified: + s += " (" + b.Since + " with uncommitted changes)" + default: + s += " (at " + b.Since + ")" + } + return s +} + +// Short is the version for a User-Agent: the version of a release, +// otherwise the label ("a1b2c3d4e5f6"). +func Short() string { + if b := Info(); b.Tag == "" && b.Commit != "" { + return b.Label + } + return Version } func short(commit string) string { diff --git a/internal/version/version_test.go b/internal/version/version_test.go index 14a9422..5aedc1e 100644 --- a/internal/version/version_test.go +++ b/internal/version/version_test.go @@ -20,3 +20,30 @@ func TestDescribe(t *testing.T) { } } } + +func TestString(t *testing.T) { + const c = "ca0f148b78b6e1f0a9b8c7d6e5f4a3b2c1d0e9f8" + build := func(tag, commit string, modified bool, desc string) Build { + b := describe("0.2.0-rc.1", tag, commit, modified) + b.Since, b.Ahead = parseDescribe(desc, b.Commit) + return b + } + for _, tc := range []struct { + b Build + want string + }{ + {build("v0.2.0-rc.1", c, false, "v0.2.0-rc.1-0-gca0f148b78b6"), "0.2.0-rc.1"}, + {build("", c, false, "v0.2.0-rc.1-3-gca0f148b78b6"), "dev ca0f148b78b6 (3 commits after v0.2.0-rc.1)"}, + {build("", c, true, "v0.2.0-rc.1-1-gca0f148b78b6"), "dev ca0f148b78b6-modified (1 commit after v0.2.0-rc.1)"}, + {build("", c, true, "v0.2.0-rc.1-0-gca0f148b78b6"), "dev ca0f148b78b6-modified (v0.2.0-rc.1 with uncommitted changes)"}, + {build("", c, false, "v0.1.0-0-gca0f148b78b6"), "dev ca0f148b78b6 (at v0.1.0)"}, + {build("", c, false, ""), "dev ca0f148b78b6"}, + // A describe of another commit (stale ldflags) is ignored. + {build("", c, false, "v0.2.0-rc.1-3-gdeadbeef0000"), "dev ca0f148b78b6"}, + {build("", "", false, ""), "0.2.0-rc.1 (source build)"}, + } { + if got := tc.b.String(); got != tc.want { + t.Errorf("String() = %q, want %q", got, tc.want) + } + } +} diff --git a/internal/web/static/assets/app.js b/internal/web/static/assets/app.js index bb8dfec..689f863 100644 --- a/internal/web/static/assets/app.js +++ b/internal/web/static/assets/app.js @@ -173,10 +173,13 @@ let modeHolder; // builds, linked to it in the repository. function footer(b) { if (!b) return null; - const what = b.tag ? "release " + b.tag - : b.commit ? "commit " + b.commit + (b.modified ? ", with uncommitted changes" : "") : "version " + b.version; + let title = b.tag ? "Jarl " + b.version + " (release " + b.tag + ")" : "Jarl " + b.version + " (built without git)"; + if (!b.tag && b.commit) { + title = "Development build of commit " + b.commit + (b.modified ? " with uncommitted changes" : ""); + if (b.since) title += b.ahead ? ", " + b.ahead + (b.ahead === 1 ? " commit" : " commits") + " after " + b.since : ", at " + b.since; + } return h("footer", { class: "site" }, "Powered by ", - h("a", { href: b.url, target: "_blank", rel: "noopener", title: "Jarl " + b.version + ", built from " + what }, "Jarl " + b.label)); + h("a", { href: b.url, target: "_blank", rel: "noopener", title }, "Jarl " + (b.tag || b.commit ? b.label : b.version))); } function renderNav() {