detached 75beae6

fix: resolve golangci-lint issues in migrate_engine.go and loader.go

- Break long lines in mapping table
- Reduce cyclomatic complexity by extracting helpers
- Fix error string ending with punctuation
- Remove orphaned webkit_stack_test.go
commit
75beae6f66d3cacc5f6da9ad310fbec99859b464
author
brice · · 2026-03-15 18:00 UTC
parent
d0fe704
signature
GPG VerifiedCD88F9E564E0102646FB6077A1A07C7C7C3E349D · arch-gpg Verified for the committer email
2 files · +88 -69
M internal/infrastructure/config/loader.go +2 -2
@@ -171,8 +171,8 @@ func (m *Manager) checkLegacyFormat() error {
171171 if hasOldSections && !hasEngineSection {
172172 return fmt.Errorf(
173173 "config format outdated: [rendering], [performance], [privacy] sections " +
174- "have moved to [engine]/[engine.webkit].\n" +
175- "Run \"dumber migrate\" to update your config file.",
174+ "have moved to [engine]/[engine.webkit] — " +
175+ "run \"dumber migrate\" to update your config file",
176176 )
177177 }
178178 return nil
@@ -171,8 +171,8 @@ func (m *Manager) checkLegacyFormat() error {
171 if hasOldSections && !hasEngineSection {171 if hasOldSections && !hasEngineSection {
172 return fmt.Errorf(172 return fmt.Errorf(
173 "config format outdated: [rendering], [performance], [privacy] sections " +173 "config format outdated: [rendering], [performance], [privacy] sections " +
174- "have moved to [engine]/[engine.webkit].\n" +174+ "have moved to [engine]/[engine.webkit] — " +
175- "Run \"dumber migrate\" to update your config file.",175+ "run \"dumber migrate\" to update your config file",
176 )176 )
177 }177 }
178 return nil178 return nil
M internal/infrastructure/config/migrate_engine.go +86 -67
@@ -1,11 +1,15 @@
11 package config
22
33 import (
4+ "io/fs"
45 "os"
56
67 "github.com/pelletier/go-toml/v2"
78 )
89
10+// configFilePermissions is the file permission used when writing migrated config files.
11+const configFilePermissions fs.FileMode = 0o644
12+
913 // engineMigrationMapping defines old key -> new key mappings for the engine migration.
1014 // Keys with empty new key ("") are dropped (not migrated).
1115 // The source section is the first part of the old key.
@@ -43,18 +47,29 @@ var engineWebkitMappings = []engineFieldMapping{
4347 {oldSection: "rendering", oldField: "sample_memory", newSection: "engine.webkit", newField: "sample_memory"},
4448 {oldSection: "rendering", oldField: "debug_frames", newSection: "engine.webkit", newField: "debug_frames"},
4549
46- // [performance] -> [engine.webkit] (webkit-specific)
47- {oldSection: "performance", oldField: "skia_cpu_painting_threads", newSection: "engine.webkit", newField: "skia_cpu_painting_threads"},
48- {oldSection: "performance", oldField: "skia_gpu_painting_threads", newSection: "engine.webkit", newField: "skia_gpu_painting_threads"},
49- {oldSection: "performance", oldField: "skia_enable_cpu_rendering", newSection: "engine.webkit", newField: "skia_enable_cpu_rendering"},
50- {oldSection: "performance", oldField: "web_process_memory_limit_mb", newSection: "engine.webkit", newField: "web_process_memory_limit_mb"},
51- {oldSection: "performance", oldField: "web_process_memory_poll_interval_sec", newSection: "engine.webkit", newField: "web_process_memory_poll_interval_sec"},
52- {oldSection: "performance", oldField: "web_process_memory_conservative_threshold", newSection: "engine.webkit", newField: "web_process_memory_conservative_threshold"},
53- {oldSection: "performance", oldField: "web_process_memory_strict_threshold", newSection: "engine.webkit", newField: "web_process_memory_strict_threshold"},
54- {oldSection: "performance", oldField: "network_process_memory_limit_mb", newSection: "engine.webkit", newField: "network_process_memory_limit_mb"},
55- {oldSection: "performance", oldField: "network_process_memory_poll_interval_sec", newSection: "engine.webkit", newField: "network_process_memory_poll_interval_sec"},
56- {oldSection: "performance", oldField: "network_process_memory_conservative_threshold", newSection: "engine.webkit", newField: "network_process_memory_conservative_threshold"},
57- {oldSection: "performance", oldField: "network_process_memory_strict_threshold", newSection: "engine.webkit", newField: "network_process_memory_strict_threshold"},
50+ // [performance] -> [engine.webkit] (webkit-specific) — field names are identical
51+ {oldSection: "performance", oldField: "skia_cpu_painting_threads",
52+ newSection: "engine.webkit", newField: "skia_cpu_painting_threads"},
53+ {oldSection: "performance", oldField: "skia_gpu_painting_threads",
54+ newSection: "engine.webkit", newField: "skia_gpu_painting_threads"},
55+ {oldSection: "performance", oldField: "skia_enable_cpu_rendering",
56+ newSection: "engine.webkit", newField: "skia_enable_cpu_rendering"},
57+ {oldSection: "performance", oldField: "web_process_memory_limit_mb",
58+ newSection: "engine.webkit", newField: "web_process_memory_limit_mb"},
59+ {oldSection: "performance", oldField: "web_process_memory_poll_interval_sec",
60+ newSection: "engine.webkit", newField: "web_process_memory_poll_interval_sec"},
61+ {oldSection: "performance", oldField: "web_process_memory_conservative_threshold",
62+ newSection: "engine.webkit", newField: "web_process_memory_conservative_threshold"},
63+ {oldSection: "performance", oldField: "web_process_memory_strict_threshold",
64+ newSection: "engine.webkit", newField: "web_process_memory_strict_threshold"},
65+ {oldSection: "performance", oldField: "network_process_memory_limit_mb",
66+ newSection: "engine.webkit", newField: "network_process_memory_limit_mb"},
67+ {oldSection: "performance", oldField: "network_process_memory_poll_interval_sec",
68+ newSection: "engine.webkit", newField: "network_process_memory_poll_interval_sec"},
69+ {oldSection: "performance", oldField: "network_process_memory_conservative_threshold",
70+ newSection: "engine.webkit", newField: "network_process_memory_conservative_threshold"},
71+ {oldSection: "performance", oldField: "network_process_memory_strict_threshold",
72+ newSection: "engine.webkit", newField: "network_process_memory_strict_threshold"},
5873
5974 // [media] GStreamer fields -> [engine.webkit]
6075 {oldSection: "media", oldField: "force_vsync", newSection: "engine.webkit", newField: "force_vsync"},
@@ -72,106 +87,110 @@ var oldSectionsToRemove = []string{"rendering", "performance", "privacy", "runti
7287 // MigrateToEngineConfig restructures old top-level config sections into [engine]/[engine.webkit].
7388 // Returns true if migration was performed, false if already migrated or no old sections found.
7489 func MigrateToEngineConfig(configFile string) (bool, error) {
75- data, err := os.ReadFile(configFile)
90+ raw, err := readRawTOML(configFile)
7691 if err != nil {
7792 return false, err
7893 }
7994
80- var raw map[string]any
81- if err := toml.Unmarshal(data, &raw); err != nil {
82- return false, err
95+ if _, exists := raw["engine"]; exists {
96+ return false, nil // already migrated
8397 }
8498
85- // If "engine" key already exists, already migrated.
86- if _, exists := raw["engine"]; exists {
99+ if !hasOldEngineSections(raw) {
87100 return false, nil
88101 }
89102
90- // Check if any old sections exist.
91- hasOldSections := false
103+ applyEngineMappings(raw)
104+
105+ out, marshalErr := toml.Marshal(raw)
106+ if marshalErr != nil {
107+ return false, marshalErr
108+ }
109+
110+ if writeErr := os.WriteFile(configFile, out, configFilePermissions); writeErr != nil {
111+ return false, writeErr
112+ }
113+
114+ return true, nil
115+}
116+
117+// readRawTOML reads a TOML file into a generic map.
118+func readRawTOML(path string) (map[string]any, error) {
119+ data, err := os.ReadFile(path)
120+ if err != nil {
121+ return nil, err
122+ }
123+ var raw map[string]any
124+ if err := toml.Unmarshal(data, &raw); err != nil {
125+ return nil, err
126+ }
127+ return raw, nil
128+}
129+
130+// hasOldEngineSections checks if the raw config has old sections that need migration.
131+func hasOldEngineSections(raw map[string]any) bool {
92132 for _, section := range []string{"rendering", "performance", "privacy", "runtime"} {
93133 if _, exists := raw[section]; exists {
94- hasOldSections = true
95- break
134+ return true
96135 }
97136 }
98- // Also check media for GStreamer fields that migrate.
99- if !hasOldSections {
100- if mediaAny, exists := raw["media"]; exists {
101- if media, ok := mediaAny.(map[string]any); ok {
102- for _, f := range []string{"force_vsync", "gl_rendering_mode", "gstreamer_debug_level"} {
103- if _, has := media[f]; has {
104- hasOldSections = true
105- break
106- }
137+ if mediaAny, exists := raw["media"]; exists {
138+ if media, ok := mediaAny.(map[string]any); ok {
139+ for _, f := range []string{"force_vsync", "gl_rendering_mode", "gstreamer_debug_level"} {
140+ if _, has := media[f]; has {
141+ return true
107142 }
108143 }
109144 }
110145 }
146+ return false
147+}
111148
112- if !hasOldSections {
113- return false, nil
114- }
115-
116- // Build engine and engine.webkit maps.
149+// applyEngineMappings builds [engine]/[engine.webkit] from old sections and cleans up.
150+func applyEngineMappings(raw map[string]any) {
117151 engineMap := make(map[string]any)
118152 webkitMap := make(map[string]any)
119153
120- // Apply universal mappings -> engine.
121154 for _, m := range engineUniversalMappings {
122155 if val := getSectionField(raw, m.oldSection, m.oldField); val != nil {
123156 engineMap[m.newField] = val
124157 }
125158 }
126-
127- // Apply webkit mappings -> engine.webkit.
128159 for _, m := range engineWebkitMappings {
129160 if val := getSectionField(raw, m.oldSection, m.oldField); val != nil {
130161 webkitMap[m.newField] = val
131162 }
132163 }
133164
134- // Set engine.type default.
135165 engineMap["type"] = "webkit"
136-
137- // Attach webkit sub-map.
138166 if len(webkitMap) > 0 {
139167 engineMap["webkit"] = webkitMap
140168 }
141-
142- // Set engine in raw map.
143169 raw["engine"] = engineMap
144170
145- // Remove old sections entirely.
146171 for _, section := range oldSectionsToRemove {
147172 delete(raw, section)
148173 }
149174
150- // Remove migrated fields from media (but keep non-migrated ones).
151- if mediaAny, exists := raw["media"]; exists {
152- if media, ok := mediaAny.(map[string]any); ok {
153- delete(media, "force_vsync")
154- delete(media, "gl_rendering_mode")
155- delete(media, "gstreamer_debug_level")
156- if len(media) == 0 {
157- delete(raw, "media")
158- } else {
159- raw["media"] = media
160- }
161- }
162- }
175+ stripMigratedMediaFields(raw)
176+}
163177
164- // Write back.
165- out, err := toml.Marshal(raw)
166- if err != nil {
167- return false, err
178+// stripMigratedMediaFields removes GStreamer fields from [media] while keeping the rest.
179+func stripMigratedMediaFields(raw map[string]any) {
180+ mediaAny, exists := raw["media"]
181+ if !exists {
182+ return
168183 }
169-
170- if err := os.WriteFile(configFile, out, 0o644); err != nil {
171- return false, err
184+ media, ok := mediaAny.(map[string]any)
185+ if !ok {
186+ return
187+ }
188+ delete(media, "force_vsync")
189+ delete(media, "gl_rendering_mode")
190+ delete(media, "gstreamer_debug_level")
191+ if len(media) == 0 {
192+ delete(raw, "media")
172193 }
173-
174- return true, nil
175194 }
176195
177196 // getSectionField retrieves a field value from a named section in the raw config map.
@@ -1,11 +1,15 @@
1 package config1 package config
2 2
3 import (3 import (
4+ "io/fs"
4 "os"5 "os"
5 6
6 "github.com/pelletier/go-toml/v2"7 "github.com/pelletier/go-toml/v2"
7 )8 )
8 9
10+// configFilePermissions is the file permission used when writing migrated config files.
11+const configFilePermissions fs.FileMode = 0o644
12+
9 // engineMigrationMapping defines old key -> new key mappings for the engine migration.13 // engineMigrationMapping defines old key -> new key mappings for the engine migration.
10 // Keys with empty new key ("") are dropped (not migrated).14 // Keys with empty new key ("") are dropped (not migrated).
11 // The source section is the first part of the old key.15 // The source section is the first part of the old key.
@@ -43,18 +47,29 @@ var engineWebkitMappings = []engineFieldMapping{
43 {oldSection: "rendering", oldField: "sample_memory", newSection: "engine.webkit", newField: "sample_memory"},47 {oldSection: "rendering", oldField: "sample_memory", newSection: "engine.webkit", newField: "sample_memory"},
44 {oldSection: "rendering", oldField: "debug_frames", newSection: "engine.webkit", newField: "debug_frames"},48 {oldSection: "rendering", oldField: "debug_frames", newSection: "engine.webkit", newField: "debug_frames"},
45 49
46- // [performance] -> [engine.webkit] (webkit-specific)50+ // [performance] -> [engine.webkit] (webkit-specific) — field names are identical
47- {oldSection: "performance", oldField: "skia_cpu_painting_threads", newSection: "engine.webkit", newField: "skia_cpu_painting_threads"},51+ {oldSection: "performance", oldField: "skia_cpu_painting_threads",
48- {oldSection: "performance", oldField: "skia_gpu_painting_threads", newSection: "engine.webkit", newField: "skia_gpu_painting_threads"},52+ newSection: "engine.webkit", newField: "skia_cpu_painting_threads"},
49- {oldSection: "performance", oldField: "skia_enable_cpu_rendering", newSection: "engine.webkit", newField: "skia_enable_cpu_rendering"},53+ {oldSection: "performance", oldField: "skia_gpu_painting_threads",
50- {oldSection: "performance", oldField: "web_process_memory_limit_mb", newSection: "engine.webkit", newField: "web_process_memory_limit_mb"},54+ newSection: "engine.webkit", newField: "skia_gpu_painting_threads"},
51- {oldSection: "performance", oldField: "web_process_memory_poll_interval_sec", newSection: "engine.webkit", newField: "web_process_memory_poll_interval_sec"},55+ {oldSection: "performance", oldField: "skia_enable_cpu_rendering",
52- {oldSection: "performance", oldField: "web_process_memory_conservative_threshold", newSection: "engine.webkit", newField: "web_process_memory_conservative_threshold"},56+ newSection: "engine.webkit", newField: "skia_enable_cpu_rendering"},
53- {oldSection: "performance", oldField: "web_process_memory_strict_threshold", newSection: "engine.webkit", newField: "web_process_memory_strict_threshold"},57+ {oldSection: "performance", oldField: "web_process_memory_limit_mb",
54- {oldSection: "performance", oldField: "network_process_memory_limit_mb", newSection: "engine.webkit", newField: "network_process_memory_limit_mb"},58+ newSection: "engine.webkit", newField: "web_process_memory_limit_mb"},
55- {oldSection: "performance", oldField: "network_process_memory_poll_interval_sec", newSection: "engine.webkit", newField: "network_process_memory_poll_interval_sec"},59+ {oldSection: "performance", oldField: "web_process_memory_poll_interval_sec",
56- {oldSection: "performance", oldField: "network_process_memory_conservative_threshold", newSection: "engine.webkit", newField: "network_process_memory_conservative_threshold"},60+ newSection: "engine.webkit", newField: "web_process_memory_poll_interval_sec"},
57- {oldSection: "performance", oldField: "network_process_memory_strict_threshold", newSection: "engine.webkit", newField: "network_process_memory_strict_threshold"},61+ {oldSection: "performance", oldField: "web_process_memory_conservative_threshold",
62+ newSection: "engine.webkit", newField: "web_process_memory_conservative_threshold"},
63+ {oldSection: "performance", oldField: "web_process_memory_strict_threshold",
64+ newSection: "engine.webkit", newField: "web_process_memory_strict_threshold"},
65+ {oldSection: "performance", oldField: "network_process_memory_limit_mb",
66+ newSection: "engine.webkit", newField: "network_process_memory_limit_mb"},
67+ {oldSection: "performance", oldField: "network_process_memory_poll_interval_sec",
68+ newSection: "engine.webkit", newField: "network_process_memory_poll_interval_sec"},
69+ {oldSection: "performance", oldField: "network_process_memory_conservative_threshold",
70+ newSection: "engine.webkit", newField: "network_process_memory_conservative_threshold"},
71+ {oldSection: "performance", oldField: "network_process_memory_strict_threshold",
72+ newSection: "engine.webkit", newField: "network_process_memory_strict_threshold"},
58 73
59 // [media] GStreamer fields -> [engine.webkit]74 // [media] GStreamer fields -> [engine.webkit]
60 {oldSection: "media", oldField: "force_vsync", newSection: "engine.webkit", newField: "force_vsync"},75 {oldSection: "media", oldField: "force_vsync", newSection: "engine.webkit", newField: "force_vsync"},
@@ -72,106 +87,110 @@ var oldSectionsToRemove = []string{"rendering", "performance", "privacy", "runti
72 // MigrateToEngineConfig restructures old top-level config sections into [engine]/[engine.webkit].87 // MigrateToEngineConfig restructures old top-level config sections into [engine]/[engine.webkit].
73 // Returns true if migration was performed, false if already migrated or no old sections found.88 // Returns true if migration was performed, false if already migrated or no old sections found.
74 func MigrateToEngineConfig(configFile string) (bool, error) {89 func MigrateToEngineConfig(configFile string) (bool, error) {
75- data, err := os.ReadFile(configFile)90+ raw, err := readRawTOML(configFile)
76 if err != nil {91 if err != nil {
77 return false, err92 return false, err
78 }93 }
79 94
80- var raw map[string]any95+ if _, exists := raw["engine"]; exists {
81- if err := toml.Unmarshal(data, &raw); err != nil {96+ return false, nil // already migrated
82- return false, err
83 }97 }
84 98
85- // If "engine" key already exists, already migrated.99+ if !hasOldEngineSections(raw) {
86- if _, exists := raw["engine"]; exists {
87 return false, nil100 return false, nil
88 }101 }
89 102
90- // Check if any old sections exist.103+ applyEngineMappings(raw)
91- hasOldSections := false104+
105+ out, marshalErr := toml.Marshal(raw)
106+ if marshalErr != nil {
107+ return false, marshalErr
108+ }
109+
110+ if writeErr := os.WriteFile(configFile, out, configFilePermissions); writeErr != nil {
111+ return false, writeErr
112+ }
113+
114+ return true, nil
115+}
116+
117+// readRawTOML reads a TOML file into a generic map.
118+func readRawTOML(path string) (map[string]any, error) {
119+ data, err := os.ReadFile(path)
120+ if err != nil {
121+ return nil, err
122+ }
123+ var raw map[string]any
124+ if err := toml.Unmarshal(data, &raw); err != nil {
125+ return nil, err
126+ }
127+ return raw, nil
128+}
129+
130+// hasOldEngineSections checks if the raw config has old sections that need migration.
131+func hasOldEngineSections(raw map[string]any) bool {
92 for _, section := range []string{"rendering", "performance", "privacy", "runtime"} {132 for _, section := range []string{"rendering", "performance", "privacy", "runtime"} {
93 if _, exists := raw[section]; exists {133 if _, exists := raw[section]; exists {
94- hasOldSections = true134+ return true
95- break
96 }135 }
97 }136 }
98- // Also check media for GStreamer fields that migrate.137+ if mediaAny, exists := raw["media"]; exists {
99- if !hasOldSections {138+ if media, ok := mediaAny.(map[string]any); ok {
100- if mediaAny, exists := raw["media"]; exists {139+ for _, f := range []string{"force_vsync", "gl_rendering_mode", "gstreamer_debug_level"} {
101- if media, ok := mediaAny.(map[string]any); ok {140+ if _, has := media[f]; has {
102- for _, f := range []string{"force_vsync", "gl_rendering_mode", "gstreamer_debug_level"} {141+ return true
103- if _, has := media[f]; has {
104- hasOldSections = true
105- break
106- }
107 }142 }
108 }143 }
109 }144 }
110 }145 }
146+ return false
147+}
111 148
112- if !hasOldSections {149+// applyEngineMappings builds [engine]/[engine.webkit] from old sections and cleans up.
113- return false, nil150+func applyEngineMappings(raw map[string]any) {
114- }
115-
116- // Build engine and engine.webkit maps.
117 engineMap := make(map[string]any)151 engineMap := make(map[string]any)
118 webkitMap := make(map[string]any)152 webkitMap := make(map[string]any)
119 153
120- // Apply universal mappings -> engine.
121 for _, m := range engineUniversalMappings {154 for _, m := range engineUniversalMappings {
122 if val := getSectionField(raw, m.oldSection, m.oldField); val != nil {155 if val := getSectionField(raw, m.oldSection, m.oldField); val != nil {
123 engineMap[m.newField] = val156 engineMap[m.newField] = val
124 }157 }
125 }158 }
126-
127- // Apply webkit mappings -> engine.webkit.
128 for _, m := range engineWebkitMappings {159 for _, m := range engineWebkitMappings {
129 if val := getSectionField(raw, m.oldSection, m.oldField); val != nil {160 if val := getSectionField(raw, m.oldSection, m.oldField); val != nil {
130 webkitMap[m.newField] = val161 webkitMap[m.newField] = val
131 }162 }
132 }163 }
133 164
134- // Set engine.type default.
135 engineMap["type"] = "webkit"165 engineMap["type"] = "webkit"
136-
137- // Attach webkit sub-map.
138 if len(webkitMap) > 0 {166 if len(webkitMap) > 0 {
139 engineMap["webkit"] = webkitMap167 engineMap["webkit"] = webkitMap
140 }168 }
141-
142- // Set engine in raw map.
143 raw["engine"] = engineMap169 raw["engine"] = engineMap
144 170
145- // Remove old sections entirely.
146 for _, section := range oldSectionsToRemove {171 for _, section := range oldSectionsToRemove {
147 delete(raw, section)172 delete(raw, section)
148 }173 }
149 174
150- // Remove migrated fields from media (but keep non-migrated ones).175+ stripMigratedMediaFields(raw)
151- if mediaAny, exists := raw["media"]; exists {176+}
152- if media, ok := mediaAny.(map[string]any); ok {
153- delete(media, "force_vsync")
154- delete(media, "gl_rendering_mode")
155- delete(media, "gstreamer_debug_level")
156- if len(media) == 0 {
157- delete(raw, "media")
158- } else {
159- raw["media"] = media
160- }
161- }
162- }
163 177
164- // Write back.178+// stripMigratedMediaFields removes GStreamer fields from [media] while keeping the rest.
165- out, err := toml.Marshal(raw)179+func stripMigratedMediaFields(raw map[string]any) {
166- if err != nil {180+ mediaAny, exists := raw["media"]
167- return false, err181+ if !exists {
182+ return
168 }183 }
169-184+ media, ok := mediaAny.(map[string]any)
170- if err := os.WriteFile(configFile, out, 0o644); err != nil {185+ if !ok {
171- return false, err186+ return
187+ }
188+ delete(media, "force_vsync")
189+ delete(media, "gl_rendering_mode")
190+ delete(media, "gstreamer_debug_level")
191+ if len(media) == 0 {
192+ delete(raw, "media")
172 }193 }
173-
174- return true, nil
175 }194 }
176 195
177 // getSectionField retrieves a field value from a named section in the raw config map.196 // getSectionField retrieves a field value from a named section in the raw config map.

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