detached 1358ff2

fix(version): resolve invalid semantic version handling

The change improves version checking reliability by:
- Adding proper handling for empty and dev versions
- Ensuring version strings are properly prefixed with 'v'
- Moving version checking to use proxy URL instead of direct GitHub API
- Restructuring version info to include proxy URL configuration
commit
1358ff2755c9a2042a4cc944e5e72d00d84f8c5f
author
Brice · · 2024-12-08 10:30 UTC
parent
3c49eb9
signature
unsigned
3 files · +91 -74
M cmd/gordon.go +3 -2
@@ -10,7 +10,6 @@ import (
1010 "github.com/bnema/gordon/internal/server"
1111 "github.com/spf13/cobra"
1212
13- // env auto load
1413 _ "github.com/joho/godotenv/autoload"
1514 )
1615
@@ -35,6 +34,8 @@ func Execute(client *cli.App, server *server.App) {
3534 }
3635
3736 func ExecuteCLI(build, commit, date string) {
37+ versionInfo := common.GetVersionInfo(build, commit, date, proxyURL)
38+
3839 buildInfo := &common.BuildConfig{
3940 BuildVersion: build,
4041 BuildCommit: commit,
@@ -54,7 +55,7 @@ func ExecuteCLI(build, commit, date string) {
5455 common.DockerInit(&s.Config.ContainerEngine)
5556
5657 // Start periodic version checking in the background
57- go common.CheckVersionPeriodically(buildInfo.BuildVersion, 3*time.Hour)
58+ go common.CheckVersionPeriodically(versionInfo, 3*time.Hour)
5859
5960 Execute(a, s)
6061 }
@@ -10,7 +10,6 @@ import (
10 "github.com/bnema/gordon/internal/server"10 "github.com/bnema/gordon/internal/server"
11 "github.com/spf13/cobra"11 "github.com/spf13/cobra"
12 12
13- // env auto load
14 _ "github.com/joho/godotenv/autoload"13 _ "github.com/joho/godotenv/autoload"
15 )14 )
16 15
@@ -35,6 +34,8 @@ func Execute(client *cli.App, server *server.App) {
35 }34 }
36 35
37 func ExecuteCLI(build, commit, date string) {36 func ExecuteCLI(build, commit, date string) {
37+ versionInfo := common.GetVersionInfo(build, commit, date, proxyURL)
38+
38 buildInfo := &common.BuildConfig{39 buildInfo := &common.BuildConfig{
39 BuildVersion: build,40 BuildVersion: build,
40 BuildCommit: commit,41 BuildCommit: commit,
@@ -54,7 +55,7 @@ func ExecuteCLI(build, commit, date string) {
54 common.DockerInit(&s.Config.ContainerEngine)55 common.DockerInit(&s.Config.ContainerEngine)
55 56
56 // Start periodic version checking in the background57 // Start periodic version checking in the background
57- go common.CheckVersionPeriodically(buildInfo.BuildVersion, 3*time.Hour)58+ go common.CheckVersionPeriodically(versionInfo, 3*time.Hour)
58 59
59 Execute(a, s)60 Execute(a, s)
60 }61 }
M internal/cli/cmd/version.go +14 -9
@@ -8,28 +8,33 @@ import (
88 "github.com/spf13/cobra"
99 )
1010
11-func NewVersionCommand(a *cli.App) *cobra.Command {
12- return &cobra.Command{
11+func NewVersionCommand(app *cli.App) *cobra.Command {
12+ cmd := &cobra.Command{
1313 Use: "version",
14- Short: "Display the version of Gordon",
14+ Short: "Print version information",
1515 Run: func(cmd *cobra.Command, args []string) {
1616 info := common.GetVersionInfo(
17- a.Config.Build.BuildVersion,
18- a.Config.Build.BuildCommit,
19- a.Config.Build.BuildDate,
17+ app.Config.Build.BuildVersion,
18+ app.Config.Build.BuildCommit,
19+ app.Config.Build.BuildDate,
20+ app.Config.Build.ProxyURL,
2021 )
2122 fmt.Println(info.String())
2223
23- // Check for updates
24- hasUpdate, latestVersion, err := common.CheckForNewVersion(info.Version)
24+ hasUpdate, latestVersion, err := common.CheckForNewVersion(
25+ info.Version,
26+ info.ProxyURL,
27+ )
2528 if err != nil {
2629 fmt.Printf("Error checking for updates: %v\n", err)
2730 return
2831 }
2932
3033 if hasUpdate {
31- fmt.Printf("\nNew version %s available! Visit: https://github.com/bnema/gordon/releases/latest\n", latestVersion)
34+ fmt.Printf("\nA new version is available: %s\n", latestVersion)
35+ fmt.Println("You can update using: gordon update")
3236 }
3337 },
3438 }
39+ return cmd
3540 }
@@ -8,28 +8,33 @@ import (
8 "github.com/spf13/cobra"8 "github.com/spf13/cobra"
9 )9 )
10 10
11-func NewVersionCommand(a *cli.App) *cobra.Command {11+func NewVersionCommand(app *cli.App) *cobra.Command {
12- return &cobra.Command{12+ cmd := &cobra.Command{
13 Use: "version",13 Use: "version",
14- Short: "Display the version of Gordon",14+ Short: "Print version information",
15 Run: func(cmd *cobra.Command, args []string) {15 Run: func(cmd *cobra.Command, args []string) {
16 info := common.GetVersionInfo(16 info := common.GetVersionInfo(
17- a.Config.Build.BuildVersion,17+ app.Config.Build.BuildVersion,
18- a.Config.Build.BuildCommit,18+ app.Config.Build.BuildCommit,
19- a.Config.Build.BuildDate,19+ app.Config.Build.BuildDate,
20+ app.Config.Build.ProxyURL,
20 )21 )
21 fmt.Println(info.String())22 fmt.Println(info.String())
22 23
23- // Check for updates24+ hasUpdate, latestVersion, err := common.CheckForNewVersion(
24- hasUpdate, latestVersion, err := common.CheckForNewVersion(info.Version)25+ info.Version,
26+ info.ProxyURL,
27+ )
25 if err != nil {28 if err != nil {
26 fmt.Printf("Error checking for updates: %v\n", err)29 fmt.Printf("Error checking for updates: %v\n", err)
27 return30 return
28 }31 }
29 32
30 if hasUpdate {33 if hasUpdate {
31- fmt.Printf("\nNew version %s available! Visit: https://github.com/bnema/gordon/releases/latest\n", latestVersion)34+ fmt.Printf("\nA new version is available: %s\n", latestVersion)
35+ fmt.Println("You can update using: gordon update")
32 }36 }
33 },37 },
34 }38 }
39+ return cmd
35 }40 }
M internal/common/version.go +74 -63
@@ -1,4 +1,5 @@
11 // internal/common/version.go
2+
23 package common
34
45 import (
@@ -14,89 +15,66 @@ import (
1415 "github.com/charmbracelet/log"
1516 )
1617
17-const (
18- githubAPI = "https://api.github.com/repos/bnema/gordon/releases/latest"
19-)
20-
2118 // Info represents version information
2219 type Info struct {
23- Version string
24- Commit string
25- Date string
20+ Version string
21+ Commit string
22+ Date string
23+ ProxyURL string
2624 }
2725
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"`
26+type ProxyVersion struct {
27+ Version string `json:"version"`
3328 }
3429
3530 // GetVersionInfo returns the current version information based on BuildConfig
36-func GetVersionInfo(buildVersion, buildCommit, buildDate string) Info {
31+func GetVersionInfo(buildVersion, buildCommit, buildDate, proxyURL string) Info {
3732 // If version was set in BuildConfig, use that
3833 if buildVersion != "" && buildVersion != "dev" {
3934 return Info{
40- Version: buildVersion,
41- Commit: buildCommit,
42- Date: buildDate,
35+ Version: buildVersion,
36+ Commit: buildCommit,
37+ Date: buildDate,
38+ ProxyURL: proxyURL,
4339 }
4440 }
4541
4642 // Try to get version from runtime build info (for go install)
4743 if buildInfo, ok := debug.ReadBuildInfo(); ok {
4844 return Info{
49- Version: buildInfo.Main.Version,
50- Commit: "unknown",
51- Date: "unknown",
45+ Version: buildInfo.Main.Version,
46+ Commit: "unknown",
47+ Date: "unknown",
48+ ProxyURL: proxyURL,
5249 }
5350 }
5451
5552 // Fallback for development builds
5653 return Info{
57- Version: "dev",
58- Commit: "unknown",
59- Date: "unknown",
60- }
61-}
62-
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)
54+ Version: "dev",
55+ Commit: "unknown",
56+ Date: "unknown",
57+ ProxyURL: proxyURL,
7358 }
74-
75- return fmt.Sprintf("Gordon version %s (commit: %s, built at: %s)",
76- i.Version, i.Commit, i.Date)
7759 }
7860
79-// CheckForNewVersion checks if a newer version is available on GitHub
80-func CheckForNewVersion(currentVersion string) (bool, string, error) {
61+// CheckForNewVersion checks if a newer version is available via proxy
62+func CheckForNewVersion(currentVersion, proxyURL string) (bool, string, error) {
8163 // Don't check for new versions in development builds or dirty builds
8264 if currentVersion == "dev" || strings.Contains(currentVersion, "-dirty") {
8365 return false, "", nil
8466 }
8567
86- // Create an HTTP client with timeout
87- client := &http.Client{
88- Timeout: 10 * time.Second,
68+ // If currentVersion is empty, treat it as v0.0.0
69+ if currentVersion == "" {
70+ currentVersion = "v0.0.0"
8971 }
9072
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)
73+ client := &http.Client{
74+ Timeout: 10 * time.Second,
9575 }
96- req.Header.Set("User-Agent", "Gordon-Version-Checker")
9776
98- // Make the request
99- resp, err := client.Do(req)
77+ resp, err := client.Get(proxyURL + "/version")
10078 if err != nil {
10179 return false, "", fmt.Errorf("error making request: %w", err)
10280 }
@@ -111,50 +89,62 @@ func CheckForNewVersion(currentVersion string) (bool, string, error) {
11189 return false, "", fmt.Errorf("error reading response: %w", err)
11290 }
11391
114- var release GitHubRelease
115- if err := json.Unmarshal(body, &release); err != nil {
92+ var proxyVer ProxyVersion
93+ if err := json.Unmarshal(body, &proxyVer); err != nil {
11694 return false, "", fmt.Errorf("error parsing JSON: %w", err)
11795 }
11896
97+ // Ensure versions start with 'v'
98+ if !strings.HasPrefix(currentVersion, "v") {
99+ currentVersion = "v" + currentVersion
100+ }
101+ if !strings.HasPrefix(proxyVer.Version, "v") {
102+ proxyVer.Version = "v" + proxyVer.Version
103+ }
104+
119105 // Clean up version strings
120- latestVersion := strings.TrimPrefix(release.TagName, "v")
106+ latestVersion := strings.TrimPrefix(proxyVer.Version, "v")
121107 currentBase := extractBaseVersion(currentVersion)
122108
109+ // Handle development versions
110+ if currentBase == "" || currentBase == "dev" {
111+ currentBase = "0.0.0"
112+ }
113+
123114 // Parse versions using semver
124115 current, err := semver.NewVersion(currentBase)
125116 if err != nil {
126- return false, "", fmt.Errorf("error parsing current version: %w", err)
117+ return false, "", fmt.Errorf("error parsing current version (%s): %w", currentBase, err)
127118 }
128119
129120 latest, err := semver.NewVersion(latestVersion)
130121 if err != nil {
131- return false, "", fmt.Errorf("error parsing latest version: %w", err)
122+ return false, "", fmt.Errorf("error parsing latest version (%s): %w", latestVersion, err)
132123 }
133124
134- // Compare versions
135125 if latest.GreaterThan(current) {
136- return true, release.TagName, nil
126+ return true, proxyVer.Version, nil
137127 }
138128
139129 return false, "", nil
140130 }
141131
142132 // CheckVersionPeriodically checks for new versions periodically
143-func CheckVersionPeriodically(currentVersion string, checkInterval time.Duration) {
133+func CheckVersionPeriodically(info Info, checkInterval time.Duration) {
144134 ticker := time.NewTicker(checkInterval)
145135 defer ticker.Stop()
146136
147137 // Initial check
148- checkVersion(currentVersion)
138+ checkVersion(info)
149139
150140 // Periodic checks
151141 for range ticker.C {
152- checkVersion(currentVersion)
142+ checkVersion(info)
153143 }
154144 }
155145
156-func checkVersion(currentVersion string) {
157- hasUpdate, latestVersion, err := CheckForNewVersion(currentVersion)
146+func checkVersion(info Info) {
147+ hasUpdate, latestVersion, err := CheckForNewVersion(info.Version, info.ProxyURL)
158148 if err != nil {
159149 log.Error("Failed to check for updates", "error", err)
160150 return
@@ -162,7 +152,7 @@ func checkVersion(currentVersion string) {
162152
163153 if hasUpdate {
164154 log.Info("New version available!",
165- "current", currentVersion,
155+ "current", info.Version,
166156 "latest", latestVersion,
167157 "update_url", "https://github.com/bnema/gordon/releases/latest")
168158 }
@@ -172,7 +162,28 @@ func extractBaseVersion(version string) string {
172162 // Remove 'v' prefix if present
173163 version = strings.TrimPrefix(version, "v")
174164
165+ // Handle empty or invalid versions
166+ if version == "" || version == "dev" {
167+ return "0.0.0"
168+ }
169+
175170 // Split on first hyphen to remove dirty/commit info
176171 parts := strings.SplitN(version, "-", 2)
177172 return parts[0]
178173 }
174+
175+// String returns a formatted version string
176+func (i Info) String() string {
177+ if i.Version == "dev" {
178+ return "Gordon version dev (development build)"
179+ }
180+
181+ // Check if it's a dirty version
182+ if strings.Contains(i.Version, "-dirty") {
183+ return fmt.Sprintf("Gordon version %s (commit: %s, built at: %s)",
184+ i.Version, i.Commit, i.Date)
185+ }
186+
187+ return fmt.Sprintf("Gordon version %s (commit: %s, built at: %s)",
188+ i.Version, i.Commit, i.Date)
189+}
@@ -1,4 +1,5 @@
1 // internal/common/version.go1 // internal/common/version.go
2+
2 package common3 package common
3 4
4 import (5 import (
@@ -14,89 +15,66 @@ import (
14 "github.com/charmbracelet/log"15 "github.com/charmbracelet/log"
15 )16 )
16 17
17-const (
18- githubAPI = "https://api.github.com/repos/bnema/gordon/releases/latest"
19-)
20-
21 // Info represents version information18 // Info represents version information
22 type Info struct {19 type Info struct {
23- Version string20+ Version string
24- Commit string21+ Commit string
25- Date string22+ Date string
23+ ProxyURL string
26 }24 }
27 25
28-// GitHubRelease represents the GitHub API response26+type ProxyVersion struct {
29-type GitHubRelease struct {27+ Version string `json:"version"`
30- TagName string `json:"tag_name"`
31- Name string `json:"name"`
32- PublishedAt time.Time `json:"published_at"`
33 }28 }
34 29
35 // GetVersionInfo returns the current version information based on BuildConfig30 // GetVersionInfo returns the current version information based on BuildConfig
36-func GetVersionInfo(buildVersion, buildCommit, buildDate string) Info {31+func GetVersionInfo(buildVersion, buildCommit, buildDate, proxyURL string) Info {
37 // If version was set in BuildConfig, use that32 // If version was set in BuildConfig, use that
38 if buildVersion != "" && buildVersion != "dev" {33 if buildVersion != "" && buildVersion != "dev" {
39 return Info{34 return Info{
40- Version: buildVersion,35+ Version: buildVersion,
41- Commit: buildCommit,36+ Commit: buildCommit,
42- Date: buildDate,37+ Date: buildDate,
38+ ProxyURL: proxyURL,
43 }39 }
44 }40 }
45 41
46 // Try to get version from runtime build info (for go install)42 // Try to get version from runtime build info (for go install)
47 if buildInfo, ok := debug.ReadBuildInfo(); ok {43 if buildInfo, ok := debug.ReadBuildInfo(); ok {
48 return Info{44 return Info{
49- Version: buildInfo.Main.Version,45+ Version: buildInfo.Main.Version,
50- Commit: "unknown",46+ Commit: "unknown",
51- Date: "unknown",47+ Date: "unknown",
48+ ProxyURL: proxyURL,
52 }49 }
53 }50 }
54 51
55 // Fallback for development builds52 // Fallback for development builds
56 return Info{53 return Info{
57- Version: "dev",54+ Version: "dev",
58- Commit: "unknown",55+ Commit: "unknown",
59- Date: "unknown",56+ Date: "unknown",
60- }57+ ProxyURL: proxyURL,
61-}
62-
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 }58 }
74-
75- return fmt.Sprintf("Gordon version %s (commit: %s, built at: %s)",
76- i.Version, i.Commit, i.Date)
77 }59 }
78 60
79-// CheckForNewVersion checks if a newer version is available on GitHub61+// CheckForNewVersion checks if a newer version is available via proxy
80-func CheckForNewVersion(currentVersion string) (bool, string, error) {62+func CheckForNewVersion(currentVersion, proxyURL string) (bool, string, error) {
81 // Don't check for new versions in development builds or dirty builds63 // Don't check for new versions in development builds or dirty builds
82 if currentVersion == "dev" || strings.Contains(currentVersion, "-dirty") {64 if currentVersion == "dev" || strings.Contains(currentVersion, "-dirty") {
83 return false, "", nil65 return false, "", nil
84 }66 }
85 67
86- // Create an HTTP client with timeout68+ // If currentVersion is empty, treat it as v0.0.0
87- client := &http.Client{69+ if currentVersion == "" {
88- Timeout: 10 * time.Second,70+ currentVersion = "v0.0.0"
89 }71 }
90 72
91- // Create request with User-Agent header (GitHub API requires this)73+ client := &http.Client{
92- req, err := http.NewRequest("GET", githubAPI, nil)74+ Timeout: 10 * time.Second,
93- if err != nil {
94- return false, "", fmt.Errorf("error creating request: %w", err)
95 }75 }
96- req.Header.Set("User-Agent", "Gordon-Version-Checker")
97 76
98- // Make the request77+ resp, err := client.Get(proxyURL + "/version")
99- resp, err := client.Do(req)
100 if err != nil {78 if err != nil {
101 return false, "", fmt.Errorf("error making request: %w", err)79 return false, "", fmt.Errorf("error making request: %w", err)
102 }80 }
@@ -111,50 +89,62 @@ func CheckForNewVersion(currentVersion string) (bool, string, error) {
111 return false, "", fmt.Errorf("error reading response: %w", err)89 return false, "", fmt.Errorf("error reading response: %w", err)
112 }90 }
113 91
114- var release GitHubRelease92+ var proxyVer ProxyVersion
115- if err := json.Unmarshal(body, &release); err != nil {93+ if err := json.Unmarshal(body, &proxyVer); err != nil {
116 return false, "", fmt.Errorf("error parsing JSON: %w", err)94 return false, "", fmt.Errorf("error parsing JSON: %w", err)
117 }95 }
118 96
97+ // Ensure versions start with 'v'
98+ if !strings.HasPrefix(currentVersion, "v") {
99+ currentVersion = "v" + currentVersion
100+ }
101+ if !strings.HasPrefix(proxyVer.Version, "v") {
102+ proxyVer.Version = "v" + proxyVer.Version
103+ }
104+
119 // Clean up version strings105 // Clean up version strings
120- latestVersion := strings.TrimPrefix(release.TagName, "v")106+ latestVersion := strings.TrimPrefix(proxyVer.Version, "v")
121 currentBase := extractBaseVersion(currentVersion)107 currentBase := extractBaseVersion(currentVersion)
122 108
109+ // Handle development versions
110+ if currentBase == "" || currentBase == "dev" {
111+ currentBase = "0.0.0"
112+ }
113+
123 // Parse versions using semver114 // Parse versions using semver
124 current, err := semver.NewVersion(currentBase)115 current, err := semver.NewVersion(currentBase)
125 if err != nil {116 if err != nil {
126- return false, "", fmt.Errorf("error parsing current version: %w", err)117+ return false, "", fmt.Errorf("error parsing current version (%s): %w", currentBase, err)
127 }118 }
128 119
129 latest, err := semver.NewVersion(latestVersion)120 latest, err := semver.NewVersion(latestVersion)
130 if err != nil {121 if err != nil {
131- return false, "", fmt.Errorf("error parsing latest version: %w", err)122+ return false, "", fmt.Errorf("error parsing latest version (%s): %w", latestVersion, err)
132 }123 }
133 124
134- // Compare versions
135 if latest.GreaterThan(current) {125 if latest.GreaterThan(current) {
136- return true, release.TagName, nil126+ return true, proxyVer.Version, nil
137 }127 }
138 128
139 return false, "", nil129 return false, "", nil
140 }130 }
141 131
142 // CheckVersionPeriodically checks for new versions periodically132 // CheckVersionPeriodically checks for new versions periodically
143-func CheckVersionPeriodically(currentVersion string, checkInterval time.Duration) {133+func CheckVersionPeriodically(info Info, checkInterval time.Duration) {
144 ticker := time.NewTicker(checkInterval)134 ticker := time.NewTicker(checkInterval)
145 defer ticker.Stop()135 defer ticker.Stop()
146 136
147 // Initial check137 // Initial check
148- checkVersion(currentVersion)138+ checkVersion(info)
149 139
150 // Periodic checks140 // Periodic checks
151 for range ticker.C {141 for range ticker.C {
152- checkVersion(currentVersion)142+ checkVersion(info)
153 }143 }
154 }144 }
155 145
156-func checkVersion(currentVersion string) {146+func checkVersion(info Info) {
157- hasUpdate, latestVersion, err := CheckForNewVersion(currentVersion)147+ hasUpdate, latestVersion, err := CheckForNewVersion(info.Version, info.ProxyURL)
158 if err != nil {148 if err != nil {
159 log.Error("Failed to check for updates", "error", err)149 log.Error("Failed to check for updates", "error", err)
160 return150 return
@@ -162,7 +152,7 @@ func checkVersion(currentVersion string) {
162 152
163 if hasUpdate {153 if hasUpdate {
164 log.Info("New version available!",154 log.Info("New version available!",
165- "current", currentVersion,155+ "current", info.Version,
166 "latest", latestVersion,156 "latest", latestVersion,
167 "update_url", "https://github.com/bnema/gordon/releases/latest")157 "update_url", "https://github.com/bnema/gordon/releases/latest")
168 }158 }
@@ -172,7 +162,28 @@ func extractBaseVersion(version string) string {
172 // Remove 'v' prefix if present162 // Remove 'v' prefix if present
173 version = strings.TrimPrefix(version, "v")163 version = strings.TrimPrefix(version, "v")
174 164
165+ // Handle empty or invalid versions
166+ if version == "" || version == "dev" {
167+ return "0.0.0"
168+ }
169+
175 // Split on first hyphen to remove dirty/commit info170 // Split on first hyphen to remove dirty/commit info
176 parts := strings.SplitN(version, "-", 2)171 parts := strings.SplitN(version, "-", 2)
177 return parts[0]172 return parts[0]
178 }173 }
174+
175+// String returns a formatted version string
176+func (i Info) String() string {
177+ if i.Version == "dev" {
178+ return "Gordon version dev (development build)"
179+ }
180+
181+ // Check if it's a dirty version
182+ if strings.Contains(i.Version, "-dirty") {
183+ return fmt.Sprintf("Gordon version %s (commit: %s, built at: %s)",
184+ i.Version, i.Commit, i.Date)
185+ }
186+
187+ return fmt.Sprintf("Gordon version %s (commit: %s, built at: %s)",
188+ i.Version, i.Commit, i.Date)
189+}

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 7

Tags 106

↑↓ moveenter openesc close