detached 4bf9f06

refactor: use version infos via ldflags and simplify version checking logic

commit
4bf9f062f38862e7ae94bb4606f49f9978602548
author
Brice · · 2024-11-20 09:09 UTC
parent
8b73233
signature
unsigned
8 files · +175 -120
M Makefile +15 -3
@@ -5,6 +5,17 @@ TAG := dev
55 DIST_DIR := ./dist
66 ENGINE := podman
77
8+# Version information
9+VERSION := $(shell git describe --tags --always --dirty)
10+COMMIT := $(shell git rev-parse --short HEAD)
11+BUILD_DATE := $(shell date -u '+%Y-%m-%d_%I:%M:%S%p')
12+
13+# Build flags
14+LDFLAGS := -s -w \
15+ -X main.version=$(VERSION) \
16+ -X main.commit=$(COMMIT) \
17+ -X main.date=$(BUILD_DATE)
18+
819 # Architectures
920 ARCHS := amd64 arm64
1021
@@ -19,8 +30,9 @@ build:
1930 @echo "Building Go binaries..."
2031 @mkdir -p $(DIST_DIR)
2132 @rm -f $(DIST_DIR)/*
22- @CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -o $(DIST_DIR)/gordon-linux-amd64 ./main.go
23- @CGO_ENABLED=0 GOOS=linux GOARCH=arm64 go build -o $(DIST_DIR)/gordon-linux-arm64 ./main.go
33+ @echo "Building with version $(VERSION), commit $(COMMIT), date $(BUILD_DATE)"
34+ @CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -ldflags="$(LDFLAGS)" -o $(DIST_DIR)/gordon-linux-amd64 ./main.go
35+ @CGO_ENABLED=0 GOOS=linux GOARCH=arm64 go build -ldflags="$(LDFLAGS)" -o $(DIST_DIR)/gordon-linux-arm64 ./main.go
2436 @echo "Go binaries built successfully"
2537
2638 # Build and push Docker images
@@ -61,4 +73,4 @@ clean:
6173 @echo "Cleaning up..."
6274 @rm -rf $(DIST_DIR)
6375 @$(ENGINE) system prune -f
64- @echo "Cleanup completed."⏎̸
76+ @echo "Cleanup completed."
@@ -5,6 +5,17 @@ TAG := dev
5 DIST_DIR := ./dist5 DIST_DIR := ./dist
6 ENGINE := podman6 ENGINE := podman
7 7
8+# Version information
9+VERSION := $(shell git describe --tags --always --dirty)
10+COMMIT := $(shell git rev-parse --short HEAD)
11+BUILD_DATE := $(shell date -u '+%Y-%m-%d_%I:%M:%S%p')
12+
13+# Build flags
14+LDFLAGS := -s -w \
15+ -X main.version=$(VERSION) \
16+ -X main.commit=$(COMMIT) \
17+ -X main.date=$(BUILD_DATE)
18+
8 # Architectures19 # Architectures
9 ARCHS := amd64 arm6420 ARCHS := amd64 arm64
10 21
@@ -19,8 +30,9 @@ build:
19 @echo "Building Go binaries..."30 @echo "Building Go binaries..."
20 @mkdir -p $(DIST_DIR)31 @mkdir -p $(DIST_DIR)
21 @rm -f $(DIST_DIR)/*32 @rm -f $(DIST_DIR)/*
22- @CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -o $(DIST_DIR)/gordon-linux-amd64 ./main.go33+ @echo "Building with version $(VERSION), commit $(COMMIT), date $(BUILD_DATE)"
23- @CGO_ENABLED=0 GOOS=linux GOARCH=arm64 go build -o $(DIST_DIR)/gordon-linux-arm64 ./main.go34+ @CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -ldflags="$(LDFLAGS)" -o $(DIST_DIR)/gordon-linux-amd64 ./main.go
35+ @CGO_ENABLED=0 GOOS=linux GOARCH=arm64 go build -ldflags="$(LDFLAGS)" -o $(DIST_DIR)/gordon-linux-arm64 ./main.go
24 @echo "Go binaries built successfully"36 @echo "Go binaries built successfully"
25 37
26 # Build and push Docker images38 # Build and push Docker images
@@ -61,4 +73,4 @@ clean:
61 @echo "Cleaning up..."73 @echo "Cleaning up..."
62 @rm -rf $(DIST_DIR)74 @rm -rf $(DIST_DIR)
63 @$(ENGINE) system prune -f75 @$(ENGINE) system prune -f
64- @echo "Cleanup completed."⏎̸76+ @echo "Cleanup completed."
M cmd/gordon.go +4 -16
@@ -2,13 +2,12 @@ package cmd
22
33 import (
44 "fmt"
5- "regexp"
5+ "time"
66
77 "github.com/bnema/gordon/internal/cli"
88 "github.com/bnema/gordon/internal/cli/cmd"
99 "github.com/bnema/gordon/internal/common"
1010 "github.com/bnema/gordon/internal/server"
11- "github.com/charmbracelet/log"
1211 "github.com/spf13/cobra"
1312
1413 // env auto load
@@ -16,9 +15,6 @@ import (
1615 )
1716
1817 var (
19- build string
20- commit string
21- date string
2218 rootCmd = &cobra.Command{Use: "gordon"}
2319 proxyURL = "https://gordon-proxy.bamen.dev"
2420 )
@@ -38,8 +34,7 @@ func Execute(client *cli.App, server *server.App) {
3834 cobra.CheckErr(rootCmd.Execute())
3935 }
4036
41-func ExecuteCLI() {
42- build = regexp.MustCompile(`\d+\.\d+\.\d+`).FindString(build)
37+func ExecuteCLI(build, commit, date string) {
4338 buildInfo := &common.BuildConfig{
4439 BuildVersion: build,
4540 BuildCommit: commit,
@@ -58,15 +53,8 @@ func ExecuteCLI() {
5853
5954 common.DockerInit(&s.Config.ContainerEngine)
6055
61- // Check for new version
62- go func() {
63- msg, err := common.CheckVersionPeriodically(&s.Config)
64- if err != nil {
65- log.Warnf("Error checking for new version: %s", err)
66- } else if msg != "" {
67- log.Info(msg)
68- }
69- }()
56+ // Start periodic version checking in the background
57+ go common.CheckVersionPeriodically(buildInfo.BuildVersion, 3*time.Hour)
7058
7159 Execute(a, s)
7260 }
@@ -2,13 +2,12 @@ package cmd
2 2
3 import (3 import (
4 "fmt"4 "fmt"
5- "regexp"5+ "time"
6 6
7 "github.com/bnema/gordon/internal/cli"7 "github.com/bnema/gordon/internal/cli"
8 "github.com/bnema/gordon/internal/cli/cmd"8 "github.com/bnema/gordon/internal/cli/cmd"
9 "github.com/bnema/gordon/internal/common"9 "github.com/bnema/gordon/internal/common"
10 "github.com/bnema/gordon/internal/server"10 "github.com/bnema/gordon/internal/server"
11- "github.com/charmbracelet/log"
12 "github.com/spf13/cobra"11 "github.com/spf13/cobra"
13 12
14 // env auto load13 // env auto load
@@ -16,9 +15,6 @@ import (
16 )15 )
17 16
18 var (17 var (
19- build string
20- commit string
21- date string
22 rootCmd = &cobra.Command{Use: "gordon"}18 rootCmd = &cobra.Command{Use: "gordon"}
23 proxyURL = "https://gordon-proxy.bamen.dev"19 proxyURL = "https://gordon-proxy.bamen.dev"
24 )20 )
@@ -38,8 +34,7 @@ func Execute(client *cli.App, server *server.App) {
38 cobra.CheckErr(rootCmd.Execute())34 cobra.CheckErr(rootCmd.Execute())
39 }35 }
40 36
41-func ExecuteCLI() {37+func ExecuteCLI(build, commit, date string) {
42- build = regexp.MustCompile(`\d+\.\d+\.\d+`).FindString(build)
43 buildInfo := &common.BuildConfig{38 buildInfo := &common.BuildConfig{
44 BuildVersion: build,39 BuildVersion: build,
45 BuildCommit: commit,40 BuildCommit: commit,
@@ -58,15 +53,8 @@ func ExecuteCLI() {
58 53
59 common.DockerInit(&s.Config.ContainerEngine)54 common.DockerInit(&s.Config.ContainerEngine)
60 55
61- // Check for new version56+ // Start periodic version checking in the background
62- go func() {57+ go common.CheckVersionPeriodically(buildInfo.BuildVersion, 3*time.Hour)
63- msg, err := common.CheckVersionPeriodically(&s.Config)
64- if err != nil {
65- log.Warnf("Error checking for new version: %s", err)
66- } else if msg != "" {
67- log.Info(msg)
68- }
69- }()
70 58
71 Execute(a, s)59 Execute(a, s)
72 }60 }
M go.mod +1 -0
@@ -4,6 +4,7 @@ go 1.23
44
55 require (
66 github.com/AlecAivazis/survey/v2 v2.3.7
7+ github.com/Masterminds/semver/v3 v3.3.1
78 github.com/PuerkitoBio/goquery v1.9.2
89 github.com/charmbracelet/bubbles v0.20.0
910 github.com/charmbracelet/lipgloss v0.13.0
@@ -4,6 +4,7 @@ go 1.23
4 4
5 require (5 require (
6 github.com/AlecAivazis/survey/v2 v2.3.76 github.com/AlecAivazis/survey/v2 v2.3.7
7+ github.com/Masterminds/semver/v3 v3.3.1
7 github.com/PuerkitoBio/goquery v1.9.28 github.com/PuerkitoBio/goquery v1.9.2
8 github.com/charmbracelet/bubbles v0.20.09 github.com/charmbracelet/bubbles v0.20.0
9 github.com/charmbracelet/lipgloss v0.13.010 github.com/charmbracelet/lipgloss v0.13.0
M go.sum +2 -0 generated file: collapsed
M internal/cli/cmd/version.go +17 -5
@@ -4,6 +4,7 @@ import (
44 "fmt"
55
66 "github.com/bnema/gordon/internal/cli"
7+ "github.com/bnema/gordon/internal/common"
78 "github.com/spf13/cobra"
89 )
910
@@ -12,11 +13,22 @@ func NewVersionCommand(a *cli.App) *cobra.Command {
1213 Use: "version",
1314 Short: "Display the version of Gordon",
1415 Run: func(cmd *cobra.Command, args []string) {
15- version := a.Config.Build.BuildVersion
16- if version == "" {
17- fmt.Println("Version: devel")
18- } else {
19- fmt.Printf("Version: %s\n", version)
16+ info := common.GetVersionInfo(
17+ a.Config.Build.BuildVersion,
18+ a.Config.Build.BuildCommit,
19+ a.Config.Build.BuildDate,
20+ )
21+ fmt.Println(info.String())
22+
23+ // Check for updates
24+ hasUpdate, latestVersion, err := common.CheckForNewVersion(info.Version)
25+ if err != nil {
26+ fmt.Printf("Error checking for updates: %v\n", err)
27+ return
28+ }
29+
30+ if hasUpdate {
31+ fmt.Printf("\nNew version %s available! Visit: https://github.com/bnema/gordon/releases/latest\n", latestVersion)
2032 }
2133 },
2234 }
@@ -4,6 +4,7 @@ import (
4 "fmt"4 "fmt"
5 5
6 "github.com/bnema/gordon/internal/cli"6 "github.com/bnema/gordon/internal/cli"
7+ "github.com/bnema/gordon/internal/common"
7 "github.com/spf13/cobra"8 "github.com/spf13/cobra"
8 )9 )
9 10
@@ -12,11 +13,22 @@ func NewVersionCommand(a *cli.App) *cobra.Command {
12 Use: "version",13 Use: "version",
13 Short: "Display the version of Gordon",14 Short: "Display the version of Gordon",
14 Run: func(cmd *cobra.Command, args []string) {15 Run: func(cmd *cobra.Command, args []string) {
15- version := a.Config.Build.BuildVersion16+ info := common.GetVersionInfo(
16- if version == "" {17+ a.Config.Build.BuildVersion,
17- fmt.Println("Version: devel")18+ a.Config.Build.BuildCommit,
18- } else {19+ a.Config.Build.BuildDate,
19- fmt.Printf("Version: %s\n", version)20+ )
21+ fmt.Println(info.String())
22+
23+ // Check for updates
24+ hasUpdate, latestVersion, err := common.CheckForNewVersion(info.Version)
25+ if err != nil {
26+ fmt.Printf("Error checking for updates: %v\n", err)
27+ return
28+ }
29+
30+ if hasUpdate {
31+ fmt.Printf("\nNew version %s available! Visit: https://github.com/bnema/gordon/releases/latest\n", latestVersion)
20 }32 }
21 },33 },
22 }34 }
M internal/common/init_config.go +5 -0
@@ -257,3 +257,8 @@ func (c *HttpConfig) FullDomain() string {
257257 }
258258 return c.Domain
259259 }
260+
261+// GetVersion
262+func (c *Config) GetVersion() string {
263+ return c.Build.BuildVersion
264+}
@@ -257,3 +257,8 @@ func (c *HttpConfig) FullDomain() string {
257 }257 }
258 return c.Domain258 return c.Domain
259 }259 }
260+
261+// GetVersion
262+func (c *Config) GetVersion() string {
263+ return c.Build.BuildVersion
264+}
M internal/common/version.go +122 -95
@@ -1,3 +1,4 @@
1+// internal/common/version.go
12 package common
23
34 import (
@@ -5,147 +6,173 @@ import (
56 "fmt"
67 "io"
78 "net/http"
8- "runtime"
9+ "runtime/debug"
10+ "strings"
911 "time"
1012
11- "github.com/bnema/gordon/pkg/docker"
13+ "github.com/Masterminds/semver/v3"
1214 "github.com/charmbracelet/log"
1315 )
1416
15-type CurrentVersion struct {
16- isContainer bool
17- Version string `json:"version"`
18-}
17+const (
18+ githubAPI = "https://api.github.com/repos/bnema/gordon/releases/latest"
19+)
1920
20-type ArchVersion struct {
21- Name string `json:"name"`
21+// Info represents version information
22+type Info struct {
23+ Version string
24+ Commit string
25+ Date string
2226 }
2327
24-type VersionInfo struct {
25- Name string `json:"name"`
28+// GitHubRelease represents the GitHub API response
29+type GitHubRelease struct {
30+ TagName string `json:"tag_name"`
31+ Name string `json:"name"`
32+ PublishedAt time.Time `json:"published_at"`
2633 }
2734
28-type VersionsResponse struct {
29- AMD64 string `json:"amd64"`
30- ARM64 string `json:"arm64"`
31-}
35+// GetVersionInfo returns the current version information based on BuildConfig
36+func GetVersionInfo(buildVersion, buildCommit, buildDate string) Info {
37+ // If version was set in BuildConfig, use that
38+ if buildVersion != "" && buildVersion != "dev" {
39+ return Info{
40+ Version: buildVersion,
41+ Commit: buildCommit,
42+ Date: buildDate,
43+ }
44+ }
3245
33-func GetCurrentBuildVersionInfo(c *Config) CurrentVersion {
34- return CurrentVersion{
35- isContainer: docker.IsRunningInContainer(),
36- Version: c.GetVersion(),
46+ // Try to get version from runtime build info (for go install)
47+ if buildInfo, ok := debug.ReadBuildInfo(); ok {
48+ return Info{
49+ Version: buildInfo.Main.Version,
50+ Commit: "unknown",
51+ Date: "unknown",
52+ }
3753 }
38-}
3954
40-func CheckForNewVersion(localVersion, remoteVersion string) string {
41- if localVersion != remoteVersion {
42- return "A new version is available"
55+ // Fallback for development builds
56+ return Info{
57+ Version: "dev",
58+ Commit: "unknown",
59+ Date: "unknown",
4360 }
44- return ""
4561 }
4662
47-func getArch() string {
48- return runtime.GOARCH
63+// String returns a formatted version string
64+func (i Info) String() string {
65+ if i.Version == "dev" {
66+ return "Gordon version dev (development build)"
67+ }
68+
69+ // Check if it's a dirty version
70+ if strings.Contains(i.Version, "-dirty") {
71+ return fmt.Sprintf("Gordon version %s (commit: %s, built at: %s)",
72+ i.Version, i.Commit, i.Date)
73+ }
74+
75+ return fmt.Sprintf("Gordon version %s (commit: %s, built at: %s)",
76+ i.Version, i.Commit, i.Date)
4977 }
5078
51-func checkAndUpdateVersion(c *Config) (string, error) {
52- localVersion := GetCurrentBuildVersionInfo(c)
53- remoteVersions, err := getRemoteVersion(c)
54- if err != nil {
55- return "", err
79+// CheckForNewVersion checks if a newer version is available on GitHub
80+func CheckForNewVersion(currentVersion string) (bool, string, error) {
81+ // Don't check for new versions in development builds or dirty builds
82+ if currentVersion == "dev" || strings.Contains(currentVersion, "-dirty") {
83+ return false, "", nil
5684 }
5785
58- var remoteVersion string
59- switch getArch() {
60- case "amd64":
61- remoteVersion = remoteVersions.AMD64
62- case "arm64":
63- remoteVersion = remoteVersions.ARM64
64- default:
65- return "", fmt.Errorf("unsupported architecture: %s", getArch())
86+ // Create an HTTP client with timeout
87+ client := &http.Client{
88+ Timeout: 10 * time.Second,
6689 }
6790
68- // Check if remoteVersion is empty
69- if remoteVersion == "" {
70- return "", fmt.Errorf("remote version not available for architecture %s", getArch())
91+ // Create request with User-Agent header (GitHub API requires this)
92+ req, err := http.NewRequest("GET", githubAPI, nil)
93+ if err != nil {
94+ return false, "", fmt.Errorf("error creating request: %w", err)
7195 }
96+ req.Header.Set("User-Agent", "Gordon-Version-Checker")
7297
73- message := CheckForNewVersion(localVersion.Version, remoteVersion)
98+ // Make the request
99+ resp, err := client.Do(req)
100+ if err != nil {
101+ return false, "", fmt.Errorf("error making request: %w", err)
102+ }
103+ defer resp.Body.Close()
74104
75- // Return the message without an error, even if it's empty
76- return message, nil
77-}
105+ if resp.StatusCode != http.StatusOK {
106+ return false, "", fmt.Errorf("unexpected status code: %d", resp.StatusCode)
107+ }
78108
79-// GetVersion returns the version
80-func (c *Config) GetVersion() string {
81- if c.Build.BuildVersion != "" {
82- return c.Build.BuildVersion
109+ body, err := io.ReadAll(resp.Body)
110+ if err != nil {
111+ return false, "", fmt.Errorf("error reading response: %w", err)
83112 }
84- return "devel"
85-}
86113
87-func CheckVersionPeriodically(c *Config) (string, error) {
88- // Check if version is "devel"
89- if c.GetVersion() == "devel" {
90- return "", nil
114+ var release GitHubRelease
115+ if err := json.Unmarshal(body, &release); err != nil {
116+ return false, "", fmt.Errorf("error parsing JSON: %w", err)
91117 }
92118
93- // Perform an immediate check.
94- newVersionMessage, err := checkAndUpdateVersion(c)
119+ // Clean up version strings
120+ latestVersion := strings.TrimPrefix(release.TagName, "v")
121+ currentBase := extractBaseVersion(currentVersion)
95122
123+ // Parse versions using semver
124+ current, err := semver.NewVersion(currentBase)
96125 if err != nil {
97- return "", err
126+ return false, "", fmt.Errorf("error parsing current version: %w", err)
98127 }
99128
100- if newVersionMessage != "" {
101- log.Info(newVersionMessage)
102- return "", nil
129+ latest, err := semver.NewVersion(latestVersion)
130+ if err != nil {
131+ return false, "", fmt.Errorf("error parsing latest version: %w", err)
103132 }
104133
105- // Set up a ticker to check every 5 minutes.
106- timer := time.NewTicker(5 * time.Minute)
107- defer timer.Stop()
108-
109- for range timer.C {
110- newVersionMessage, err = checkAndUpdateVersion(c)
111- if err != nil {
112- log.Printf("Error fetching remote version: %s", err)
113- continue
114- }
115-
116- if newVersionMessage != "" {
117- fmt.Println(newVersionMessage)
118- return "", nil //We return so the loop stops in case of a new version
119- }
134+ // Compare versions
135+ if latest.GreaterThan(current) {
136+ return true, release.TagName, nil
120137 }
121138
122- return "", nil
139+ return false, "", nil
123140 }
124141
125-func getRemoteVersion(c *Config) (VersionsResponse, error) {
126- proxyURL := c.Build.ProxyURL
127- if proxyURL == "" {
128- return VersionsResponse{}, fmt.Errorf("proxy URL is empty")
129- }
142+// CheckVersionPeriodically checks for new versions periodically
143+func CheckVersionPeriodically(currentVersion string, checkInterval time.Duration) {
144+ ticker := time.NewTicker(checkInterval)
145+ defer ticker.Stop()
130146
131- checkVersionEndpoint := proxyURL + "/version"
132- resp, err := http.Get(checkVersionEndpoint)
133- if err != nil {
134- return VersionsResponse{}, err
135- }
147+ // Initial check
148+ checkVersion(currentVersion)
136149
137- defer resp.Body.Close()
150+ // Periodic checks
151+ for range ticker.C {
152+ checkVersion(currentVersion)
153+ }
154+}
138155
139- body, err := io.ReadAll(resp.Body)
156+func checkVersion(currentVersion string) {
157+ hasUpdate, latestVersion, err := CheckForNewVersion(currentVersion)
140158 if err != nil {
141- return VersionsResponse{}, err
159+ log.Error("Failed to check for updates", "error", err)
160+ return
142161 }
143162
144- var remoteVersionInfo VersionsResponse
145- err = json.Unmarshal(body, &remoteVersionInfo)
146- if err != nil {
147- return VersionsResponse{}, err
163+ if hasUpdate {
164+ log.Info("New version available!",
165+ "current", currentVersion,
166+ "latest", latestVersion,
167+ "update_url", "https://github.com/bnema/gordon/releases/latest")
148168 }
169+}
170+
171+func extractBaseVersion(version string) string {
172+ // Remove 'v' prefix if present
173+ version = strings.TrimPrefix(version, "v")
149174
150- return remoteVersionInfo, nil
175+ // Split on first hyphen to remove dirty/commit info
176+ parts := strings.SplitN(version, "-", 2)
177+ return parts[0]
151178 }
@@ -1,3 +1,4 @@
1+// internal/common/version.go
1 package common2 package common
2 3
3 import (4 import (
@@ -5,147 +6,173 @@ import (
5 "fmt"6 "fmt"
6 "io"7 "io"
7 "net/http"8 "net/http"
8- "runtime"9+ "runtime/debug"
10+ "strings"
9 "time"11 "time"
10 12
11- "github.com/bnema/gordon/pkg/docker"13+ "github.com/Masterminds/semver/v3"
12 "github.com/charmbracelet/log"14 "github.com/charmbracelet/log"
13 )15 )
14 16
15-type CurrentVersion struct {17+const (
16- isContainer bool18+ githubAPI = "https://api.github.com/repos/bnema/gordon/releases/latest"
17- Version string `json:"version"`19+)
18-}
19 20
20-type ArchVersion struct {21+// Info represents version information
21- Name string `json:"name"`22+type Info struct {
23+ Version string
24+ Commit string
25+ Date string
22 }26 }
23 27
24-type VersionInfo struct {28+// GitHubRelease represents the GitHub API response
25- Name string `json:"name"`29+type GitHubRelease struct {
30+ TagName string `json:"tag_name"`
31+ Name string `json:"name"`
32+ PublishedAt time.Time `json:"published_at"`
26 }33 }
27 34
28-type VersionsResponse struct {35+// GetVersionInfo returns the current version information based on BuildConfig
29- AMD64 string `json:"amd64"`36+func GetVersionInfo(buildVersion, buildCommit, buildDate string) Info {
30- ARM64 string `json:"arm64"`37+ // If version was set in BuildConfig, use that
31-}38+ if buildVersion != "" && buildVersion != "dev" {
39+ return Info{
40+ Version: buildVersion,
41+ Commit: buildCommit,
42+ Date: buildDate,
43+ }
44+ }
32 45
33-func GetCurrentBuildVersionInfo(c *Config) CurrentVersion {46+ // Try to get version from runtime build info (for go install)
34- return CurrentVersion{47+ if buildInfo, ok := debug.ReadBuildInfo(); ok {
35- isContainer: docker.IsRunningInContainer(),48+ return Info{
36- Version: c.GetVersion(),49+ Version: buildInfo.Main.Version,
50+ Commit: "unknown",
51+ Date: "unknown",
52+ }
37 }53 }
38-}
39 54
40-func CheckForNewVersion(localVersion, remoteVersion string) string {55+ // Fallback for development builds
41- if localVersion != remoteVersion {56+ return Info{
42- return "A new version is available"57+ Version: "dev",
58+ Commit: "unknown",
59+ Date: "unknown",
43 }60 }
44- return ""
45 }61 }
46 62
47-func getArch() string {63+// String returns a formatted version string
48- return runtime.GOARCH64+func (i Info) String() string {
65+ if i.Version == "dev" {
66+ return "Gordon version dev (development build)"
67+ }
68+
69+ // Check if it's a dirty version
70+ if strings.Contains(i.Version, "-dirty") {
71+ return fmt.Sprintf("Gordon version %s (commit: %s, built at: %s)",
72+ i.Version, i.Commit, i.Date)
73+ }
74+
75+ return fmt.Sprintf("Gordon version %s (commit: %s, built at: %s)",
76+ i.Version, i.Commit, i.Date)
49 }77 }
50 78
51-func checkAndUpdateVersion(c *Config) (string, error) {79+// CheckForNewVersion checks if a newer version is available on GitHub
52- localVersion := GetCurrentBuildVersionInfo(c)80+func CheckForNewVersion(currentVersion string) (bool, string, error) {
53- remoteVersions, err := getRemoteVersion(c)81+ // Don't check for new versions in development builds or dirty builds
54- if err != nil {82+ if currentVersion == "dev" || strings.Contains(currentVersion, "-dirty") {
55- return "", err83+ return false, "", nil
56 }84 }
57 85
58- var remoteVersion string86+ // Create an HTTP client with timeout
59- switch getArch() {87+ client := &http.Client{
60- case "amd64":88+ Timeout: 10 * time.Second,
61- remoteVersion = remoteVersions.AMD64
62- case "arm64":
63- remoteVersion = remoteVersions.ARM64
64- default:
65- return "", fmt.Errorf("unsupported architecture: %s", getArch())
66 }89 }
67 90
68- // Check if remoteVersion is empty91+ // Create request with User-Agent header (GitHub API requires this)
69- if remoteVersion == "" {92+ req, err := http.NewRequest("GET", githubAPI, nil)
70- return "", fmt.Errorf("remote version not available for architecture %s", getArch())93+ if err != nil {
94+ return false, "", fmt.Errorf("error creating request: %w", err)
71 }95 }
96+ req.Header.Set("User-Agent", "Gordon-Version-Checker")
72 97
73- message := CheckForNewVersion(localVersion.Version, remoteVersion)98+ // Make the request
99+ resp, err := client.Do(req)
100+ if err != nil {
101+ return false, "", fmt.Errorf("error making request: %w", err)
102+ }
103+ defer resp.Body.Close()
74 104
75- // Return the message without an error, even if it's empty105+ if resp.StatusCode != http.StatusOK {
76- return message, nil106+ return false, "", fmt.Errorf("unexpected status code: %d", resp.StatusCode)
77-}107+ }
78 108
79-// GetVersion returns the version109+ body, err := io.ReadAll(resp.Body)
80-func (c *Config) GetVersion() string {110+ if err != nil {
81- if c.Build.BuildVersion != "" {111+ return false, "", fmt.Errorf("error reading response: %w", err)
82- return c.Build.BuildVersion
83 }112 }
84- return "devel"
85-}
86 113
87-func CheckVersionPeriodically(c *Config) (string, error) {114+ var release GitHubRelease
88- // Check if version is "devel"115+ if err := json.Unmarshal(body, &release); err != nil {
89- if c.GetVersion() == "devel" {116+ return false, "", fmt.Errorf("error parsing JSON: %w", err)
90- return "", nil
91 }117 }
92 118
93- // Perform an immediate check.119+ // Clean up version strings
94- newVersionMessage, err := checkAndUpdateVersion(c)120+ latestVersion := strings.TrimPrefix(release.TagName, "v")
121+ currentBase := extractBaseVersion(currentVersion)
95 122
123+ // Parse versions using semver
124+ current, err := semver.NewVersion(currentBase)
96 if err != nil {125 if err != nil {
97- return "", err126+ return false, "", fmt.Errorf("error parsing current version: %w", err)
98 }127 }
99 128
100- if newVersionMessage != "" {129+ latest, err := semver.NewVersion(latestVersion)
101- log.Info(newVersionMessage)130+ if err != nil {
102- return "", nil131+ return false, "", fmt.Errorf("error parsing latest version: %w", err)
103 }132 }
104 133
105- // Set up a ticker to check every 5 minutes.134+ // Compare versions
106- timer := time.NewTicker(5 * time.Minute)135+ if latest.GreaterThan(current) {
107- defer timer.Stop()136+ return true, release.TagName, nil
108-
109- for range timer.C {
110- newVersionMessage, err = checkAndUpdateVersion(c)
111- if err != nil {
112- log.Printf("Error fetching remote version: %s", err)
113- continue
114- }
115-
116- if newVersionMessage != "" {
117- fmt.Println(newVersionMessage)
118- return "", nil //We return so the loop stops in case of a new version
119- }
120 }137 }
121 138
122- return "", nil139+ return false, "", nil
123 }140 }
124 141
125-func getRemoteVersion(c *Config) (VersionsResponse, error) {142+// CheckVersionPeriodically checks for new versions periodically
126- proxyURL := c.Build.ProxyURL143+func CheckVersionPeriodically(currentVersion string, checkInterval time.Duration) {
127- if proxyURL == "" {144+ ticker := time.NewTicker(checkInterval)
128- return VersionsResponse{}, fmt.Errorf("proxy URL is empty")145+ defer ticker.Stop()
129- }
130 146
131- checkVersionEndpoint := proxyURL + "/version"147+ // Initial check
132- resp, err := http.Get(checkVersionEndpoint)148+ checkVersion(currentVersion)
133- if err != nil {
134- return VersionsResponse{}, err
135- }
136 149
137- defer resp.Body.Close()150+ // Periodic checks
151+ for range ticker.C {
152+ checkVersion(currentVersion)
153+ }
154+}
138 155
139- body, err := io.ReadAll(resp.Body)156+func checkVersion(currentVersion string) {
157+ hasUpdate, latestVersion, err := CheckForNewVersion(currentVersion)
140 if err != nil {158 if err != nil {
141- return VersionsResponse{}, err159+ log.Error("Failed to check for updates", "error", err)
160+ return
142 }161 }
143 162
144- var remoteVersionInfo VersionsResponse163+ if hasUpdate {
145- err = json.Unmarshal(body, &remoteVersionInfo)164+ log.Info("New version available!",
146- if err != nil {165+ "current", currentVersion,
147- return VersionsResponse{}, err166+ "latest", latestVersion,
167+ "update_url", "https://github.com/bnema/gordon/releases/latest")
148 }168 }
169+}
170+
171+func extractBaseVersion(version string) string {
172+ // Remove 'v' prefix if present
173+ version = strings.TrimPrefix(version, "v")
149 174
150- return remoteVersionInfo, nil175+ // Split on first hyphen to remove dirty/commit info
176+ parts := strings.SplitN(version, "-", 2)
177+ return parts[0]
151 }178 }
M main.go +9 -1
@@ -2,8 +2,16 @@ package main
22
33 import (
44 "github.com/bnema/gordon/cmd"
5+ "github.com/charmbracelet/log"
6+)
7+
8+var (
9+ version string
10+ commit string
11+ date string
512 )
613
714 func main() {
8- cmd.ExecuteCLI()
15+ log.Info("Starting Gordon", "version", version, "commit", commit, "date", date)
16+ cmd.ExecuteCLI(version, commit, date)
917 }
@@ -2,8 +2,16 @@ package main
2 2
3 import (3 import (
4 "github.com/bnema/gordon/cmd"4 "github.com/bnema/gordon/cmd"
5+ "github.com/charmbracelet/log"
6+)
7+
8+var (
9+ version string
10+ commit string
11+ date string
5 )12 )
6 13
7 func main() {14 func main() {
8- cmd.ExecuteCLI()15+ log.Info("Starting Gordon", "version", version, "commit", commit, "date", date)
16+ cmd.ExecuteCLI(version, commit, date)
9 }17 }

Keyboard shortcuts

Single-key shortcuts are on. ctrl k and esc work whatever this says.

ctrl k :
go to a section, branch, tag, file or repository
?
this list
j k
move the row cursor down / up
enter
open the row under the cursor; on Files, a directory opens or closes in place
↑ ↓ ← → home end
in the file tree (focus on a row): previous / next row, close / open a directory or go to its parent / first entry, first / last row. They never open a file
l h
on Files: open a directory or a file / close a directory or go up to its parent, then to the parent page. Elsewhere: into the side panel (on a merge request, its files) / back to the content; there j k move between its controls
g g G
first / last row
/
filter the list; on Files, find a file anywhere in the repository (esc clears, enter opens the best match)
b
switch branch or tag
g s g f g c g b g m g i g r g a
tabs: summary, files, commits, branches & tags, merge requests, issues, releases, actions (when available)
g h
home
w
wide mode: use the whole width of the screen, or not (remembered)
y
copy a permalink pinned to the commit
d u x
diff: split / unified / collapse or expand all
e E
diff: the whole file in view / every file, with the lines the hunks hide
n
merge request: next note in the diff
esc
close a dialog, leave a field

Branches 8

Tags 106

↑↓ moveenter openesc close