Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add version flag #160

Merged
merged 3 commits into from
Jan 29, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .gitattributes
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
# Include version information on `git archive'
/version/version.go export-subst
14 changes: 5 additions & 9 deletions internal/specialcmd/specialcmd.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,12 +10,14 @@ package specialcmd
import (
_ "embed"
"fmt"
"github.com/janpfeifer/gonb/internal/jpyexec"
"golang.org/x/exp/slices"
"os"
"strings"
"time"

"github.com/janpfeifer/gonb/internal/jpyexec"
"github.com/janpfeifer/gonb/version"
"golang.org/x/exp/slices"

. "github.com/janpfeifer/gonb/common"
"github.com/janpfeifer/gonb/gonbui/protocol"
"github.com/janpfeifer/gonb/internal/goexec"
Expand Down Expand Up @@ -239,13 +241,7 @@ func execSpecialConfig(msg kernel.Message, goExec *goexec.State, cmdStr string,
klog.Errorf("Failed publishing help contents: %+v", err)
}
case "version":
gitHash := os.Getenv(protocol.GONB_GIT_COMMIT)
gitVersion := os.Getenv(protocol.GONB_VERSION)
gitCommitURL := fmt.Sprintf("https://github.com/janpfeifer/gonb/tree/%s", gitHash)
gitTagURL := fmt.Sprintf("https://github.com/janpfeifer/gonb/releases/tag/%s", gitVersion)

err := kernel.PublishMarkdown(msg, fmt.Sprintf(
"**GoNB** version [%s](%s) / Commit: [%s](%s)\n", gitVersion, gitTagURL, gitHash, gitCommitURL))
err := kernel.PublishMarkdown(msg, version.AppVersion.Markdown())
if err != nil {
klog.Errorf("Failed publishing version contents: %+v", err)
}
Expand Down
136 changes: 136 additions & 0 deletions internal/version/version.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,136 @@
package version

import (
"fmt"
"runtime"
"runtime/debug"
"strconv"
"strings"
)

type VersionInfo struct {
Version string
Commit string
CommitLink string
ReleaseLink string
}

const (
BaseVersionControlURL string = "https://github.com/janpfeifer/gonb"
)

// AppVersion determines version and commit information based on multiple data sources:
// - AppVersion information dynamically added by `git archive` in the remaining to parameters.
// - A hardcoded version number passed as first parameter.
// - Commit information added to the binary by `go build`.
//
// It's supposed to be called like this in combination with setting the `export-subst` attribute for the corresponding
// file in .gitattributes:
//
// var AppVersion = version.AppVersion("1.0.0-rc1", "$Format:%(describe)$", "$Format:%H$")
//
// When exported using `git archive`, the placeholders are replaced in the file and this version information is
// preferred. Otherwise the hardcoded version is used and augmented with commit information from the build metadata.
//
// Source: https://github.com/Icinga/icingadb/blob/51068fff46364385f3c0165aab7b7393fa6a303b/pkg/version/version.go
func AppVersion(version, gitVersion, gitHash string) *VersionInfo {
if !strings.HasPrefix(gitVersion, "$") && !strings.HasPrefix(gitHash, "$") {
versionInfo := &VersionInfo{
Version: gitVersion,
Commit: gitHash,
ReleaseLink: fmt.Sprintf("%s/release/%s", BaseVersionControlURL, gitVersion),
}
if len(gitHash) > 0 {
versionInfo.CommitLink = fmt.Sprintf("%s/tree/%s", BaseVersionControlURL, gitHash)
}

return versionInfo
} else {
var commit string
var releaseVersion string

if info, ok := debug.ReadBuildInfo(); ok {
modified := false

for _, setting := range info.Settings {
switch setting.Key {
case "vcs.revision":
commit = setting.Value
case "vcs.modified":
modified, _ = strconv.ParseBool(setting.Value)
}
if strings.Contains(setting.Key, "ldflags") &&
strings.Contains(setting.Value, "git.tag") {

start := strings.Index(setting.Value, "git.tag=") + 8
end := strings.Index(setting.Value[start:], "'") + start
version = setting.Value[start:end]
}
}

// Same truncation length for the commit hash
const hashLen = 7
releaseVersion = version

if len(commit) >= hashLen {
if modified {
version += "-dirty"
commit += " (modified)"
}
}
}

versionInfo := &VersionInfo{
Version: version,
Commit: commit,
ReleaseLink: fmt.Sprintf("%s/release/%s", BaseVersionControlURL, releaseVersion),
}
if len(commit) > 0 {
versionInfo.CommitLink = fmt.Sprintf("%s/tree/%s", BaseVersionControlURL, commit)
}

return versionInfo
}
}

// GetInfo Get version info
func (v *VersionInfo) GetInfo() VersionInfo {
return *v
}

// String Get version as a string
func (v *VersionInfo) String() string {
return v.Version
}

// Print writes verbose version output to stdout.
func (v *VersionInfo) Print() {
fmt.Println("GoNB version:", v.Version)
fmt.Println()

if len(v.CommitLink) > 0 {
fmt.Println("Version control info:")
fmt.Printf(" Commit: %s \n", v.CommitLink)
fmt.Printf(" Release: %s \n", v.ReleaseLink)
fmt.Println()
}

fmt.Println("Build info:")
fmt.Printf(" Go version: %s (OS: %s, arch: %s)\n", runtime.Version(), runtime.GOOS, runtime.GOARCH)
}

func (v *VersionInfo) Markdown() string {
var markdown string
markdown += fmt.Sprintf("## GoNB version: `%s`\n\n", v.Version)

if len(v.CommitLink) > 0 {
markdown += "### Version Control Info\n"
markdown += fmt.Sprintf("- Commit: [%s](%s)\n", v.Commit, v.CommitLink)
markdown += fmt.Sprintf("- Release: [%s](%s)\n\n", v.Version, v.ReleaseLink)
}

markdown += "### Build Info\n"
markdown += fmt.Sprintf("- Go version: %s (OS: %s, Arch: %s)\n", runtime.Version(), runtime.GOOS, runtime.GOARCH)

return markdown
}
133 changes: 133 additions & 0 deletions internal/version/version_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,133 @@
package version

import (
"bytes"
"os"
"runtime"
"strings"
"testing"
)

func TestAppVersion(t *testing.T) {
tests := []struct {
name string
version string
gitDescribe string
gitHash string
want *VersionInfo
}{
{
name: "With git information",
version: "1.0.0",
gitDescribe: "v1.0.0",
gitHash: "abc1234",
want: &VersionInfo{
Version: "v1.0.0",
Commit: "abc1234",
CommitLink: "https://github.com/janpfeifer/gonb/tree/abc1234",
},
},
{
name: "Without git information",
version: "1.0.0",
gitDescribe: "$Format:%(describe)$",
gitHash: "$Format:%H$",
want: &VersionInfo{
Version: "1.0.0",
},
},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got := AppVersion(tt.version, tt.gitDescribe, tt.gitHash)

if got.Version != tt.want.Version {
t.Errorf("AppVersion().Version = %v, want %v", got.Version, tt.want.Version)
}

if got.Commit != tt.want.Commit {
t.Errorf("AppVersion().Commit = %v, want %v", got.Commit, tt.want.Commit)
}

if got.CommitLink != tt.want.CommitLink {
t.Errorf("AppVersion().VersionControlLink = %v, want %v", got.CommitLink, tt.want.CommitLink)
}
})
}
}

func TestVersionInfo_GetInfo(t *testing.T) {
v := &VersionInfo{
Version: "1.0.0",
Commit: "abc123",
CommitLink: "https://github.com/janpfeifer/gonb/tree/abc123",
}

got := v.GetInfo()
if got != *v {
t.Errorf("VersionInfo.GetInfo() = %v, want %v", got, *v)
}
}

func TestVersionInfo_String(t *testing.T) {
v := &VersionInfo{
Version: "1.0.0",
Commit: "abc123",
CommitLink: "https://github.com/janpfeifer/gonb/tree/abc123",
}

got := v.String()
if got != v.Version {
t.Errorf("VersionInfo.String() = %v, want %v", got, v.Version)
}
}

func TestVersionInfo_Print(t *testing.T) {
v := &VersionInfo{
Version: "1.0.0",
Commit: "abc123",
CommitLink: "https://github.com/janpfeifer/gonb/tree/abc123",
}

// Capture output to verify it contains expected information
output := captureOutput(func() {
v.Print()
})

// Check if output contains expected information
expectedStrings := []string{
"GoNB version: 1.0.0",
"Version control info:",
v.CommitLink,
"Build info:",
runtime.Version(),
runtime.GOOS,
runtime.GOARCH,
}

for _, expected := range expectedStrings {
if !strings.Contains(output, expected) {
t.Errorf("Print() output missing expected string: %s", expected)
}
}
}

// Helper function to capture stdout dynamically
func captureOutput(f func()) string {
// Redirect stdout to a buffer
old := os.Stdout
r, w, _ := os.Pipe()
os.Stdout = w

// Call the function
f()

// Restore stdout and read buffer
w.Close()
os.Stdout = old

var buf bytes.Buffer
_, _ = buf.ReadFrom(r)
return buf.String()
}
Loading