detached 895ec25

fix(arch): address coderabbit cleanup findings

commit
895ec2552a011037a204a9713c677257774f028c
author
brice · · 2026-06-25 16:14 UTC
parent
573527d
signature
GPG VerifiedCD88F9E564E0102646FB6077A1A07C7C7C3E349D · arch-gpg Verified for the committer email
10 files · +69 -29
M docs/config/index.md +3 -0
@@ -592,6 +592,7 @@ Dumber automatically detects your GPU vendor (AMD/Intel/NVIDIA) and sets optimal
592592 - **NVIDIA**: Uses `nvidia` driver with EGL platform
593593
594594 **Example:**
595+
595596 ```toml
596597 [media]
597598 hardware_decoding = "auto" # HW preferred, SW fallback
@@ -600,6 +601,7 @@ show_diagnostics = false # Keep off for daily use
600601 ```
601602
602603 **WebKit fallback GStreamer diagnostics:**
604+
603605 ```toml
604606 [engine.webkit]
605607 gstreamer_debug_level = 3
@@ -608,6 +610,7 @@ gstreamer_debug_level = 3
608610 Set `media.show_diagnostics = false` and `engine.webkit.gstreamer_debug_level = 0` to return to normal mode.
609611
610612 **Diagnostics CLI:**
613+
611614 ```bash
612615 # Check GStreamer plugins and VA-API status
613616 dumber doctor --media
@@ -592,6 +592,7 @@ Dumber automatically detects your GPU vendor (AMD/Intel/NVIDIA) and sets optimal
592 - **NVIDIA**: Uses `nvidia` driver with EGL platform592 - **NVIDIA**: Uses `nvidia` driver with EGL platform
593 593
594 **Example:**594 **Example:**
595+
595 ```toml596 ```toml
596 [media]597 [media]
597 hardware_decoding = "auto" # HW preferred, SW fallback598 hardware_decoding = "auto" # HW preferred, SW fallback
@@ -600,6 +601,7 @@ show_diagnostics = false # Keep off for daily use
600 ```601 ```
601 602
602 **WebKit fallback GStreamer diagnostics:**603 **WebKit fallback GStreamer diagnostics:**
604+
603 ```toml605 ```toml
604 [engine.webkit]606 [engine.webkit]
605 gstreamer_debug_level = 3607 gstreamer_debug_level = 3
@@ -608,6 +610,7 @@ gstreamer_debug_level = 3
608 Set `media.show_diagnostics = false` and `engine.webkit.gstreamer_debug_level = 0` to return to normal mode.610 Set `media.show_diagnostics = false` and `engine.webkit.gstreamer_debug_level = 0` to return to normal mode.
609 611
610 **Diagnostics CLI:**612 **Diagnostics CLI:**
613+
611 ```bash614 ```bash
612 # Check GStreamer plugins and VA-API status615 # Check GStreamer plugins and VA-API status
613 dumber doctor --media616 dumber doctor --media
M docs/reference/troubleshooting.md +5 -6
@@ -14,14 +14,13 @@ dumber doctor --media # GStreamer/VA-API only
1414
1515 ### Browser won't start
1616
17-**Symptoms:** Error about missing GUI runtime libraries or GTK/WebKitGTK fallback version
17+**Symptoms:** Error about the default CEF runtime, GTK libraries, or WebKitGTK fallback version.
1818
1919 **Solution:**
20-1. Run `dumber doctor --runtime`
21-2. Install missing dependencies:
22- - Arch: `pacman -S cef webkitgtk-6.0 gtk4`
23- - Fedora: install GTK4 and WebKitGTK packages from your distribution and a CEF runtime/package if available
24- - Ubuntu: `apt install libwebkitgtk-6.0-4 libgtk-4-1` and provide a CEF runtime for the default backend
20+1. Run `dumber doctor --runtime`.
21+2. Match the fix to the backend in the error:
22+ - **Default CEF backend:** install or provide a CEF runtime. On Arch, use `pacman -S cef`. On Fedora or Ubuntu, install an available CEF runtime/package or set `engine.cef.cef_dir` to a downloaded runtime.
23+ - **GTK/WebKit fallback backend:** install GTK4 and WebKitGTK. On Arch, use `pacman -S webkitgtk-6.0 gtk4`. On Fedora, install GTK4 and WebKitGTK packages from your distribution. On Ubuntu, use `apt install libwebkitgtk-6.0-4 libgtk-4-1`.
2524
2625 ### Video playback issues
2726
@@ -14,14 +14,13 @@ dumber doctor --media # GStreamer/VA-API only
14 14
15 ### Browser won't start15 ### Browser won't start
16 16
17-**Symptoms:** Error about missing GUI runtime libraries or GTK/WebKitGTK fallback version17+**Symptoms:** Error about the default CEF runtime, GTK libraries, or WebKitGTK fallback version.
18 18
19 **Solution:**19 **Solution:**
20-1. Run `dumber doctor --runtime`20+1. Run `dumber doctor --runtime`.
21-2. Install missing dependencies:21+2. Match the fix to the backend in the error:
22- - Arch: `pacman -S cef webkitgtk-6.0 gtk4`22+ - **Default CEF backend:** install or provide a CEF runtime. On Arch, use `pacman -S cef`. On Fedora or Ubuntu, install an available CEF runtime/package or set `engine.cef.cef_dir` to a downloaded runtime.
23- - Fedora: install GTK4 and WebKitGTK packages from your distribution and a CEF runtime/package if available23+ - **GTK/WebKit fallback backend:** install GTK4 and WebKitGTK. On Arch, use `pacman -S webkitgtk-6.0 gtk4`. On Fedora, install GTK4 and WebKitGTK packages from your distribution. On Ubuntu, use `apt install libwebkitgtk-6.0-4 libgtk-4-1`.
24- - Ubuntu: `apt install libwebkitgtk-6.0-4 libgtk-4-1` and provide a CEF runtime for the default backend
25 24
26 ### Video playback issues25 ### Video playback issues
27 26
M internal/application/usecase/navigation_url_normalizer.go +2 -1
@@ -2,6 +2,7 @@ package usecase
22
33 import (
44 "context"
5+ stdurl "net/url"
56 "strings"
67
78 "github.com/bnema/dumber/internal/application/port"
@@ -27,7 +28,7 @@ func (n *NavigationURLNormalizer) Normalize(ctx context.Context, input string) s
2728 }
2829 if n != nil && n.localPaths != nil && shouldProbeLocalPath(input) {
2930 if absPath, ok, err := n.localPaths.ResolveExistingPath(ctx, input); err == nil && ok {
30- return "file://" + absPath
31+ return (&stdurl.URL{Scheme: "file", Path: absPath}).String()
3132 }
3233 }
3334 return domainurl.Normalize(input)
@@ -2,6 +2,7 @@ package usecase
2 2
3 import (3 import (
4 "context"4 "context"
5+ stdurl "net/url"
5 "strings"6 "strings"
6 7
7 "github.com/bnema/dumber/internal/application/port"8 "github.com/bnema/dumber/internal/application/port"
@@ -27,7 +28,7 @@ func (n *NavigationURLNormalizer) Normalize(ctx context.Context, input string) s
27 }28 }
28 if n != nil && n.localPaths != nil && shouldProbeLocalPath(input) {29 if n != nil && n.localPaths != nil && shouldProbeLocalPath(input) {
29 if absPath, ok, err := n.localPaths.ResolveExistingPath(ctx, input); err == nil && ok {30 if absPath, ok, err := n.localPaths.ResolveExistingPath(ctx, input); err == nil && ok {
30- return "file://" + absPath31+ return (&stdurl.URL{Scheme: "file", Path: absPath}).String()
31 }32 }
32 }33 }
33 return domainurl.Normalize(input)34 return domainurl.Normalize(input)
M internal/application/usecase/navigation_url_normalizer_test.go +2 -14
@@ -3,23 +3,11 @@ package usecase
33 import (
44 "context"
55 "errors"
6+ stdurl "net/url"
67 "path/filepath"
78 "testing"
89 )
910
10-type fakeLocalPathResolver struct {
11- paths map[string]string
12- err error
13-}
14-
15-func (r fakeLocalPathResolver) ResolveExistingPath(_ context.Context, input string) (string, bool, error) {
16- if r.err != nil {
17- return "", false, r.err
18- }
19- abs, ok := r.paths[input]
20- return abs, ok, nil
21-}
22-
2311 func TestNavigationURLNormalizerResolvesExistingLocalPaths(t *testing.T) {
2412 ctx := context.Background()
2513 absFile := filepath.Join(string(filepath.Separator), "tmp", "page.html")
@@ -32,7 +20,7 @@ func TestNavigationURLNormalizerResolvesExistingLocalPaths(t *testing.T) {
3220 for _, input := range []string{absFile, "page.html", "~/page.html"} {
3321 t.Run(input, func(t *testing.T) {
3422 got := normalizer.Normalize(ctx, input)
35- want := "file://" + absFile
23+ want := (&stdurl.URL{Scheme: "file", Path: absFile}).String()
3624 if got != want {
3725 t.Fatalf("Normalize(%q) = %q, want %q", input, got, want)
3826 }
@@ -3,23 +3,11 @@ package usecase
3 import (3 import (
4 "context"4 "context"
5 "errors"5 "errors"
6+ stdurl "net/url"
6 "path/filepath"7 "path/filepath"
7 "testing"8 "testing"
8 )9 )
9 10
10-type fakeLocalPathResolver struct {
11- paths map[string]string
12- err error
13-}
14-
15-func (r fakeLocalPathResolver) ResolveExistingPath(_ context.Context, input string) (string, bool, error) {
16- if r.err != nil {
17- return "", false, r.err
18- }
19- abs, ok := r.paths[input]
20- return abs, ok, nil
21-}
22-
23 func TestNavigationURLNormalizerResolvesExistingLocalPaths(t *testing.T) {11 func TestNavigationURLNormalizerResolvesExistingLocalPaths(t *testing.T) {
24 ctx := context.Background()12 ctx := context.Background()
25 absFile := filepath.Join(string(filepath.Separator), "tmp", "page.html")13 absFile := filepath.Join(string(filepath.Separator), "tmp", "page.html")
@@ -32,7 +20,7 @@ func TestNavigationURLNormalizerResolvesExistingLocalPaths(t *testing.T) {
32 for _, input := range []string{absFile, "page.html", "~/page.html"} {20 for _, input := range []string{absFile, "page.html", "~/page.html"} {
33 t.Run(input, func(t *testing.T) {21 t.Run(input, func(t *testing.T) {
34 got := normalizer.Normalize(ctx, input)22 got := normalizer.Normalize(ctx, input)
35- want := "file://" + absFile23+ want := (&stdurl.URL{Scheme: "file", Path: absFile}).String()
36 if got != want {24 if got != want {
37 t.Fatalf("Normalize(%q) = %q, want %q", input, got, want)25 t.Fatalf("Normalize(%q) = %q, want %q", input, got, want)
38 }26 }
M internal/application/usecase/navigation_url_usecase_integration_test.go +13 -0
@@ -8,6 +8,19 @@ import (
88 "github.com/bnema/dumber/internal/domain/entity"
99 )
1010
11+type fakeLocalPathResolver struct {
12+ paths map[string]string
13+ err error
14+}
15+
16+func (r fakeLocalPathResolver) ResolveExistingPath(_ context.Context, input string) (string, bool, error) {
17+ if r.err != nil {
18+ return "", false, r.err
19+ }
20+ abs, ok := r.paths[input]
21+ return abs, ok, nil
22+}
23+
1124 func TestManageTabsCreateResolvesExistingLocalInitialURL(t *testing.T) {
1225 ctx := context.Background()
1326 tabs := entity.NewTabList()
@@ -8,6 +8,19 @@ import (
8 "github.com/bnema/dumber/internal/domain/entity"8 "github.com/bnema/dumber/internal/domain/entity"
9 )9 )
10 10
11+type fakeLocalPathResolver struct {
12+ paths map[string]string
13+ err error
14+}
15+
16+func (r fakeLocalPathResolver) ResolveExistingPath(_ context.Context, input string) (string, bool, error) {
17+ if r.err != nil {
18+ return "", false, r.err
19+ }
20+ abs, ok := r.paths[input]
21+ return abs, ok, nil
22+}
23+
11 func TestManageTabsCreateResolvesExistingLocalInitialURL(t *testing.T) {24 func TestManageTabsCreateResolvesExistingLocalInitialURL(t *testing.T) {
12 ctx := context.Background()25 ctx := context.Background()
13 tabs := entity.NewTabList()26 tabs := entity.NewTabList()
M internal/infrastructure/config/loader.go +3 -3
@@ -325,10 +325,10 @@ func (m *Manager) checkLegacyFormat() error {
325325
326326 if hasOldSections && !hasEngineSection {
327327 return fmt.Errorf(
328- "config format outdated: legacy engine keys in [rendering], [performance], " +
329- "[privacy], [runtime], or migrated [media] fields have moved to " +
330- "[engine]/[engine.webkit] — run \"dumber config migrate\" " +
328+ "config format outdated: legacy engine keys in %s have moved to "+
329+ "[engine]/[engine.webkit] — run \"dumber config migrate\" "+
331330 "to update your config file",
331+ legacyEngineInputSectionsMessage(),
332332 )
333333 }
334334 return nil
@@ -325,10 +325,10 @@ func (m *Manager) checkLegacyFormat() error {
325 325
326 if hasOldSections && !hasEngineSection {326 if hasOldSections && !hasEngineSection {
327 return fmt.Errorf(327 return fmt.Errorf(
328- "config format outdated: legacy engine keys in [rendering], [performance], " +328+ "config format outdated: legacy engine keys in %s have moved to "+
329- "[privacy], [runtime], or migrated [media] fields have moved to " +329+ "[engine]/[engine.webkit] — run \"dumber config migrate\" "+
330- "[engine]/[engine.webkit] — run \"dumber config migrate\" " +
331 "to update your config file",330 "to update your config file",
331+ legacyEngineInputSectionsMessage(),
332 )332 )
333 }333 }
334 return nil334 return nil
M internal/infrastructure/config/migrate_engine.go +27 -3
@@ -2,9 +2,11 @@ package config
22
33 import (
44 "fmt"
5- "github.com/pelletier/go-toml/v2"
65 "io/fs"
76 "os"
7+ "strings"
8+
9+ "github.com/pelletier/go-toml/v2"
810 )
911
1012 // configFilePermissions is the file permission used when writing migrated config files.
@@ -119,6 +121,28 @@ func legacyEngineSectionsToRemove() []string {
119121 return sections
120122 }
121123
124+func legacyEngineInputSections() []string {
125+ sections := make([]string, 0)
126+ seen := make(map[string]struct{})
127+ for _, alias := range legacyEngineAliases {
128+ if _, ok := seen[alias.legacySection]; ok {
129+ continue
130+ }
131+ seen[alias.legacySection] = struct{}{}
132+ sections = append(sections, alias.legacySection)
133+ }
134+ return sections
135+}
136+
137+func legacyEngineInputSectionsMessage() string {
138+ sections := legacyEngineInputSections()
139+ formatted := make([]string, 0, len(sections))
140+ for _, section := range sections {
141+ formatted = append(formatted, "["+section+"]")
142+ }
143+ return strings.Join(formatted, ", ")
144+}
145+
122146 func hasLegacyEngineAlias(raw map[string]any, alias legacyEngineAlias) bool {
123147 sectionAny, exists := raw[alias.legacySection]
124148 if !exists {
@@ -222,9 +246,9 @@ func applyEngineMappings(raw map[string]any) {
222246 continue
223247 }
224248 switch alias.targetSection {
225- case "engine":
249+ case engineTargetSection:
226250 engineMap[alias.targetField] = val
227- case "engine.webkit":
251+ case engineWebKitTargetSection:
228252 webkitMap[alias.targetField] = val
229253 }
230254 }
@@ -2,9 +2,11 @@ package config
2 2
3 import (3 import (
4 "fmt"4 "fmt"
5- "github.com/pelletier/go-toml/v2"
6 "io/fs"5 "io/fs"
7 "os"6 "os"
7+ "strings"
8+
9+ "github.com/pelletier/go-toml/v2"
8 )10 )
9 11
10 // configFilePermissions is the file permission used when writing migrated config files.12 // configFilePermissions is the file permission used when writing migrated config files.
@@ -119,6 +121,28 @@ func legacyEngineSectionsToRemove() []string {
119 return sections121 return sections
120 }122 }
121 123
124+func legacyEngineInputSections() []string {
125+ sections := make([]string, 0)
126+ seen := make(map[string]struct{})
127+ for _, alias := range legacyEngineAliases {
128+ if _, ok := seen[alias.legacySection]; ok {
129+ continue
130+ }
131+ seen[alias.legacySection] = struct{}{}
132+ sections = append(sections, alias.legacySection)
133+ }
134+ return sections
135+}
136+
137+func legacyEngineInputSectionsMessage() string {
138+ sections := legacyEngineInputSections()
139+ formatted := make([]string, 0, len(sections))
140+ for _, section := range sections {
141+ formatted = append(formatted, "["+section+"]")
142+ }
143+ return strings.Join(formatted, ", ")
144+}
145+
122 func hasLegacyEngineAlias(raw map[string]any, alias legacyEngineAlias) bool {146 func hasLegacyEngineAlias(raw map[string]any, alias legacyEngineAlias) bool {
123 sectionAny, exists := raw[alias.legacySection]147 sectionAny, exists := raw[alias.legacySection]
124 if !exists {148 if !exists {
@@ -222,9 +246,9 @@ func applyEngineMappings(raw map[string]any) {
222 continue246 continue
223 }247 }
224 switch alias.targetSection {248 switch alias.targetSection {
225- case "engine":249+ case engineTargetSection:
226 engineMap[alias.targetField] = val250 engineMap[alias.targetField] = val
227- case "engine.webkit":251+ case engineWebKitTargetSection:
228 webkitMap[alias.targetField] = val252 webkitMap[alias.targetField] = val
229 }253 }
230 }254 }
M internal/infrastructure/config/schema_provider_test.go +2 -0
@@ -37,6 +37,8 @@ func configurationReferenceKeys(t *testing.T) map[string]struct{} {
3737 data, err := os.ReadFile(docPath)
3838 require.NoError(t, err)
3939
40+ // Keep this split point aligned with the markdown heading so legacy-key rows
41+ // stay out of the canonical schema key coverage set.
4042 canonicalReference, _, _ := strings.Cut(string(data), "Legacy key migration:")
4143 rowRE := regexp.MustCompile(`(?m)^\| ` + "`" + `([^` + "`" + `]+)` + "`" + ` \|`)
4244 keys := make(map[string]struct{})
@@ -37,6 +37,8 @@ func configurationReferenceKeys(t *testing.T) map[string]struct{} {
37 data, err := os.ReadFile(docPath)37 data, err := os.ReadFile(docPath)
38 require.NoError(t, err)38 require.NoError(t, err)
39 39
40+ // Keep this split point aligned with the markdown heading so legacy-key rows
41+ // stay out of the canonical schema key coverage set.
40 canonicalReference, _, _ := strings.Cut(string(data), "Legacy key migration:")42 canonicalReference, _, _ := strings.Cut(string(data), "Legacy key migration:")
41 rowRE := regexp.MustCompile(`(?m)^\| ` + "`" + `([^` + "`" + `]+)` + "`" + ` \|`)43 rowRE := regexp.MustCompile(`(?m)^\| ` + "`" + `([^` + "`" + `]+)` + "`" + ` \|`)
42 keys := make(map[string]struct{})44 keys := make(map[string]struct{})
M internal/infrastructure/config/validation_test.go +2 -0
@@ -295,6 +295,7 @@ func TestValidateConfig_CEFConfig(t *testing.T) {
295295 }
296296
297297 func TestValidateConfig_WorkspaceNewPaneURLAllowsExistingLocalPathLikeValues(t *testing.T) {
298+ // This test mutates process CWD; do not add t.Parallel here.
298299 tmpDir := t.TempDir()
299300 oldWD, err := os.Getwd()
300301 require.NoError(t, err)
@@ -330,6 +331,7 @@ func TestValidateConfig_WorkspaceNewPaneURLRejectsMissingLocalPathLikeValues(t *
330331 }
331332
332333 func TestValidateConfig_WorkspaceNewPaneURLAllowsExistingBareRelativeFile(t *testing.T) {
334+ // This test mutates process CWD; do not add t.Parallel here.
333335 tmpDir := t.TempDir()
334336 oldWD, err := os.Getwd()
335337 require.NoError(t, err)
@@ -295,6 +295,7 @@ func TestValidateConfig_CEFConfig(t *testing.T) {
295 }295 }
296 296
297 func TestValidateConfig_WorkspaceNewPaneURLAllowsExistingLocalPathLikeValues(t *testing.T) {297 func TestValidateConfig_WorkspaceNewPaneURLAllowsExistingLocalPathLikeValues(t *testing.T) {
298+ // This test mutates process CWD; do not add t.Parallel here.
298 tmpDir := t.TempDir()299 tmpDir := t.TempDir()
299 oldWD, err := os.Getwd()300 oldWD, err := os.Getwd()
300 require.NoError(t, err)301 require.NoError(t, err)
@@ -330,6 +331,7 @@ func TestValidateConfig_WorkspaceNewPaneURLRejectsMissingLocalPathLikeValues(t *
330 }331 }
331 332
332 func TestValidateConfig_WorkspaceNewPaneURLAllowsExistingBareRelativeFile(t *testing.T) {333 func TestValidateConfig_WorkspaceNewPaneURLAllowsExistingBareRelativeFile(t *testing.T) {
334+ // This test mutates process CWD; do not add t.Parallel here.
333 tmpDir := t.TempDir()335 tmpDir := t.TempDir()
334 oldWD, err := os.Getwd()336 oldWD, err := os.Getwd()
335 require.NoError(t, err)337 require.NoError(t, err)
M internal/infrastructure/filesystem/local_path_resolver_test.go +10 -2
@@ -15,6 +15,10 @@ func TestResolveExistingPath(t *testing.T) {
1515 if err := os.WriteFile(file, []byte("ok"), 0644); err != nil {
1616 t.Fatal(err)
1717 }
18+ wantFile, err := filepath.EvalSymlinks(file)
19+ if err != nil {
20+ t.Fatal(err)
21+ }
1822
1923 oldWD, err := os.Getwd()
2024 if err != nil {
@@ -45,8 +49,12 @@ func TestResolveExistingPath(t *testing.T) {
4549 if !ok {
4650 t.Fatalf("ResolveExistingPath ok=false")
4751 }
48- if got != file {
49- t.Fatalf("ResolveExistingPath(%q) = %q, want %q", tt.input, got, file)
52+ gotFile, err := filepath.EvalSymlinks(got)
53+ if err != nil {
54+ t.Fatal(err)
55+ }
56+ if gotFile != wantFile {
57+ t.Fatalf("ResolveExistingPath(%q) = %q, want %q", tt.input, gotFile, wantFile)
5058 }
5159 })
5260 }
@@ -15,6 +15,10 @@ func TestResolveExistingPath(t *testing.T) {
15 if err := os.WriteFile(file, []byte("ok"), 0644); err != nil {15 if err := os.WriteFile(file, []byte("ok"), 0644); err != nil {
16 t.Fatal(err)16 t.Fatal(err)
17 }17 }
18+ wantFile, err := filepath.EvalSymlinks(file)
19+ if err != nil {
20+ t.Fatal(err)
21+ }
18 22
19 oldWD, err := os.Getwd()23 oldWD, err := os.Getwd()
20 if err != nil {24 if err != nil {
@@ -45,8 +49,12 @@ func TestResolveExistingPath(t *testing.T) {
45 if !ok {49 if !ok {
46 t.Fatalf("ResolveExistingPath ok=false")50 t.Fatalf("ResolveExistingPath ok=false")
47 }51 }
48- if got != file {52+ gotFile, err := filepath.EvalSymlinks(got)
49- t.Fatalf("ResolveExistingPath(%q) = %q, want %q", tt.input, got, file)53+ if err != nil {
54+ t.Fatal(err)
55+ }
56+ if gotFile != wantFile {
57+ t.Fatalf("ResolveExistingPath(%q) = %q, want %q", tt.input, gotFile, wantFile)
50 }58 }
51 })59 })
52 }60 }

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 42

↑↓ moveenter openesc close