detached 2fa804a

GIR: Implement support for multiple libraries to be in `shared-library` key in GIR by iterating over all registered ones in the GIR and using the first one that linking succeeds with

Signed-off-by: Felicitas Pojtinger <felicitas@pojtinger.com>
commit
2fa804aa19107cdfdf7f446b17491b83171c64d5
author
Felicitas Pojtinger · · 2025-10-16 03:14 UTC
committer
Jeroen Wijenbergh · · 2025-10-22 07:42 UTC
parent
edf23b7
signature
unsigned
5 files · +118 -26
M internal/core/core.go +75 -5
@@ -44,7 +44,7 @@ var names = map[string][]string{
4444 "CAIRO": {"libcairo.so"},
4545 "GDKPIXBUF": {"libgdk_pixbuf-2.0.so"},
4646 "GIO": {"libgio-2.0.so"},
47- "GLIB": {"libglib-2.0.so"},
47+ "GLIB": {"libglib-2.0.so", "libgobject-2.0.so"},
4848 "GMODULE": {"libgmodule-2.0.so"},
4949 "GOBJECT": {"libgobject-2.0.so"},
5050 "GRAPHENE": {"libgraphene-1.0.so"},
@@ -85,12 +85,12 @@ func SetPackageName(libName, pkgName string) {
8585 }
8686 }
8787
88-// SetSharedLibrary registers a shared library name for a library.
88+// SetSharedLibraries registers shared library names for a library.
8989 // This is used by the code generator to set library names from GIR files.
9090 // It won't override existing entries to preserve defaults.
91-func SetSharedLibrary(libName, sharedLib string) {
92- if _, exists := names[libName]; !exists && sharedLib != "" {
93- names[libName] = []string{sharedLib}
91+func SetSharedLibraries(libName string, sharedLibs []string) {
92+ if _, exists := names[libName]; !exists && len(sharedLibs) > 0 {
93+ names[libName] = sharedLibs
9494 }
9595 }
9696
@@ -205,3 +205,73 @@ func GoString(c uintptr) string {
205205 }
206206 return string(unsafe.Slice((*byte)(ptr), length))
207207 }
208+
209+// GetPaths gets all shared object file paths for a library name that may have multiple libraries.
210+// This is used when a package needs to load multiple shared libraries (e.g., GLIB needs both libgobject and libglib).
211+// Returns a slice of full paths to all libraries for this package.
212+func GetPaths(name string) []string {
213+ // resolve alias
214+ if v, ok := aliases[name]; ok {
215+ name = v
216+ }
217+
218+ libNames, ok := names[name]
219+ if !ok || len(libNames) == 0 {
220+ // Fallback to single GetPath for backward compatibility
221+ return []string{GetPath(name)}
222+ }
223+
224+ // If only one library, use the original GetPath logic
225+ if len(libNames) == 1 {
226+ return []string{GetPath(name)}
227+ }
228+
229+ var result []string
230+ for _, libName := range libNames {
231+ // Try PUREGOTK_LIB_FOLDER first
232+ ep := os.Getenv("PUREGOTK_LIB_FOLDER")
233+ if ep != "" {
234+ suffixes := []string{"", ".0", ".1", ".2"}
235+ found := false
236+ for _, s := range suffixes {
237+ fullPath := filepath.Join(ep, libName+s)
238+ if _, err := os.Stat(fullPath); err == nil {
239+ result = append(result, fullPath)
240+ found = true
241+ break
242+ }
243+ }
244+ if found {
245+ continue
246+ }
247+ }
248+
249+ // Try hardcoded paths
250+ gp, pathsOk := paths[runtime.GOARCH]
251+ if pathsOk {
252+ found := false
253+ for _, p := range gp {
254+ suffixes := []string{"", ".0", ".1", ".2"}
255+ fn := filepath.Join(p, libName)
256+ for _, s := range suffixes {
257+ if _, err := os.Stat(fn + s); err == nil {
258+ result = append(result, fn+s)
259+ found = true
260+ break
261+ }
262+ }
263+ if found {
264+ break
265+ }
266+ }
267+ if found {
268+ continue
269+ }
270+ }
271+
272+ // If we get here, we couldn't find this library
273+ panic(fmt.Sprintf("Path for library %s (package %s) not found. Please set PUREGOTK_LIB_FOLDER or ensure the library is in standard paths", libName, name))
274+ }
275+
276+ return result
277+}
@@ -44,7 +44,7 @@ var names = map[string][]string{
44 "CAIRO": {"libcairo.so"},44 "CAIRO": {"libcairo.so"},
45 "GDKPIXBUF": {"libgdk_pixbuf-2.0.so"},45 "GDKPIXBUF": {"libgdk_pixbuf-2.0.so"},
46 "GIO": {"libgio-2.0.so"},46 "GIO": {"libgio-2.0.so"},
47- "GLIB": {"libglib-2.0.so"},47+ "GLIB": {"libglib-2.0.so", "libgobject-2.0.so"},
48 "GMODULE": {"libgmodule-2.0.so"},48 "GMODULE": {"libgmodule-2.0.so"},
49 "GOBJECT": {"libgobject-2.0.so"},49 "GOBJECT": {"libgobject-2.0.so"},
50 "GRAPHENE": {"libgraphene-1.0.so"},50 "GRAPHENE": {"libgraphene-1.0.so"},
@@ -85,12 +85,12 @@ func SetPackageName(libName, pkgName string) {
85 }85 }
86 }86 }
87 87
88-// SetSharedLibrary registers a shared library name for a library.88+// SetSharedLibraries registers shared library names for a library.
89 // This is used by the code generator to set library names from GIR files.89 // This is used by the code generator to set library names from GIR files.
90 // It won't override existing entries to preserve defaults.90 // It won't override existing entries to preserve defaults.
91-func SetSharedLibrary(libName, sharedLib string) {91+func SetSharedLibraries(libName string, sharedLibs []string) {
92- if _, exists := names[libName]; !exists && sharedLib != "" {92+ if _, exists := names[libName]; !exists && len(sharedLibs) > 0 {
93- names[libName] = []string{sharedLib}93+ names[libName] = sharedLibs
94 }94 }
95 }95 }
96 96
@@ -205,3 +205,73 @@ func GoString(c uintptr) string {
205 }205 }
206 return string(unsafe.Slice((*byte)(ptr), length))206 return string(unsafe.Slice((*byte)(ptr), length))
207 }207 }
208+
209+// GetPaths gets all shared object file paths for a library name that may have multiple libraries.
210+// This is used when a package needs to load multiple shared libraries (e.g., GLIB needs both libgobject and libglib).
211+// Returns a slice of full paths to all libraries for this package.
212+func GetPaths(name string) []string {
213+ // resolve alias
214+ if v, ok := aliases[name]; ok {
215+ name = v
216+ }
217+
218+ libNames, ok := names[name]
219+ if !ok || len(libNames) == 0 {
220+ // Fallback to single GetPath for backward compatibility
221+ return []string{GetPath(name)}
222+ }
223+
224+ // If only one library, use the original GetPath logic
225+ if len(libNames) == 1 {
226+ return []string{GetPath(name)}
227+ }
228+
229+ var result []string
230+ for _, libName := range libNames {
231+ // Try PUREGOTK_LIB_FOLDER first
232+ ep := os.Getenv("PUREGOTK_LIB_FOLDER")
233+ if ep != "" {
234+ suffixes := []string{"", ".0", ".1", ".2"}
235+ found := false
236+ for _, s := range suffixes {
237+ fullPath := filepath.Join(ep, libName+s)
238+ if _, err := os.Stat(fullPath); err == nil {
239+ result = append(result, fullPath)
240+ found = true
241+ break
242+ }
243+ }
244+ if found {
245+ continue
246+ }
247+ }
248+
249+ // Try hardcoded paths
250+ gp, pathsOk := paths[runtime.GOARCH]
251+ if pathsOk {
252+ found := false
253+ for _, p := range gp {
254+ suffixes := []string{"", ".0", ".1", ".2"}
255+ fn := filepath.Join(p, libName)
256+ for _, s := range suffixes {
257+ if _, err := os.Stat(fn + s); err == nil {
258+ result = append(result, fn+s)
259+ found = true
260+ break
261+ }
262+ }
263+ if found {
264+ break
265+ }
266+ }
267+ if found {
268+ continue
269+ }
270+ }
271+
272+ // If we get here, we couldn't find this library
273+ panic(fmt.Sprintf("Path for library %s (package %s) not found. Please set PUREGOTK_LIB_FOLDER or ensure the library is in standard paths", libName, name))
274+ }
275+
276+ return result
277+}
M internal/gir/pass/pass.go +9 -2
@@ -385,7 +385,14 @@ func (p *Pass) writeGo(r types.Repository, gotemp *template.Template, dir string
385385 pkgConfigName = r.Packages[0].Name
386386 }
387387
388- sharedLibrary := ns.SharedLibrary
388+ var sharedLibraries []string
389+ if ns.SharedLibrary != "" {
390+ for _, lib := range libs := strings.Split(ns.SharedLibrary, ",") {
391+ if trimmed := strings.TrimSpace(lib); trimmed != "" {
392+ sharedLibraries = append(sharedLibraries, trimmed)
393+ }
394+ }
395+ }
389396
390397 for _, fn := range files {
391398 methods := 0
@@ -409,7 +416,7 @@ func (p *Pass) writeGo(r types.Repository, gotemp *template.Template, dir string
409416 PkgName: pkgName,
410417 PkgEnv: strings.ToUpper(pkgName),
411418 PkgConfigName: pkgConfigName,
412- SharedLibrary: sharedLibrary,
419+ SharedLibraries: sharedLibraries,
413420 NeedsInit: needsInit,
414421 Aliases: aliases[fn],
415422 Callbacks: callbacks[fn],
@@ -385,7 +385,14 @@ func (p *Pass) writeGo(r types.Repository, gotemp *template.Template, dir string
385 pkgConfigName = r.Packages[0].Name385 pkgConfigName = r.Packages[0].Name
386 }386 }
387 387
388- sharedLibrary := ns.SharedLibrary388+ var sharedLibraries []string
389+ if ns.SharedLibrary != "" {
390+ for _, lib := range libs := strings.Split(ns.SharedLibrary, ",") {
391+ if trimmed := strings.TrimSpace(lib); trimmed != "" {
392+ sharedLibraries = append(sharedLibraries, trimmed)
393+ }
394+ }
395+ }
389 396
390 for _, fn := range files {397 for _, fn := range files {
391 methods := 0398 methods := 0
@@ -409,7 +416,7 @@ func (p *Pass) writeGo(r types.Repository, gotemp *template.Template, dir string
409 PkgName: pkgName,416 PkgName: pkgName,
410 PkgEnv: strings.ToUpper(pkgName),417 PkgEnv: strings.ToUpper(pkgName),
411 PkgConfigName: pkgConfigName,418 PkgConfigName: pkgConfigName,
412- SharedLibrary: sharedLibrary,419+ SharedLibraries: sharedLibraries,
413 NeedsInit: needsInit,420 NeedsInit: needsInit,
414 Aliases: aliases[fn],421 Aliases: aliases[fn],
415 Callbacks: callbacks[fn],422 Callbacks: callbacks[fn],
M internal/gir/types/template.go +2 -2
@@ -428,8 +428,8 @@ type TemplateArg struct {
428428 PkgEnv string
429429 // PkgConfigName is the pkg-config package name from the GIR file
430430 PkgConfigName string
431- // SharedLibrary is the shared library name from the GIR file
432- SharedLibrary string
431+ // SharedLibraries is the list of shared library names from the GIR file
432+ SharedLibraries []string
433433 // NeedsInit declares whether or not this file needs an init code to register functions with purego
434434 NeedsInit bool
435435 // Imports defines the package imports that we need
@@ -428,8 +428,8 @@ type TemplateArg struct {
428 PkgEnv string428 PkgEnv string
429 // PkgConfigName is the pkg-config package name from the GIR file429 // PkgConfigName is the pkg-config package name from the GIR file
430 PkgConfigName string430 PkgConfigName string
431- // SharedLibrary is the shared library name from the GIR file431+ // SharedLibraries is the list of shared library names from the GIR file
432- SharedLibrary string432+ SharedLibraries []string
433 // NeedsInit declares whether or not this file needs an init code to register functions with purego433 // NeedsInit declares whether or not this file needs an init code to register functions with purego
434 NeedsInit bool434 NeedsInit bool
435 // Imports defines the package imports that we need435 // Imports defines the package imports that we need
M pkg/core/core.go +2 -1
@@ -5,7 +5,8 @@ import "github.com/jwijenbergh/puregotk/internal/core"
55 var (
66 PuregoSafeRegister = core.PuregoSafeRegister
77 GetPath = core.GetPath
8+ GetPaths = core.GetPaths
89 GoString = core.GoString
910 SetPackageName = core.SetPackageName
10- SetSharedLibrary = core.SetSharedLibrary
11+ SetSharedLibraries = core.SetSharedLibraries
1112 )
@@ -5,7 +5,8 @@ import "github.com/jwijenbergh/puregotk/internal/core"
5 var (5 var (
6 PuregoSafeRegister = core.PuregoSafeRegister6 PuregoSafeRegister = core.PuregoSafeRegister
7 GetPath = core.GetPath7 GetPath = core.GetPath
8+ GetPaths = core.GetPaths
8 GoString = core.GoString9 GoString = core.GoString
9 SetPackageName = core.SetPackageName10 SetPackageName = core.SetPackageName
10- SetSharedLibrary = core.SetSharedLibrary11+ SetSharedLibraries = core.SetSharedLibraries
11 )12 )
M templates/go +30 -16
@@ -335,62 +335,76 @@ func init() {
335335 {{if .PkgConfigName -}}
336336 core.SetPackageName("{{.PkgEnv}}", "{{.PkgConfigName}}")
337337 {{end -}}
338- {{if .SharedLibrary -}}
339- core.SetSharedLibrary("{{.PkgEnv}}", "{{.SharedLibrary}}")
338+ {{if .SharedLibraries -}}
339+ core.SetSharedLibraries("{{.PkgEnv}}", []string{ {{range .SharedLibraries}}"{{.}}", {{end}} })
340340 {{end -}}
341- lib, err := purego.Dlopen(core.GetPath("{{.PkgEnv}}"), purego.RTLD_NOW|purego.RTLD_GLOBAL)
342- if err != nil {
341+
342+ var libs []uintptr
343+ for _, libPath := range core.GetPaths("{{.PkgEnv}}") {
344+ lib, err := purego.Dlopen(libPath, purego.RTLD_NOW|purego.RTLD_GLOBAL)
345+ if err != nil {
343346 panic(err)
347+ }
348+ libs = append(libs, lib)
349+ }
350+
351+ tryRegister := func(fptr interface{}, name string) {
352+ for _, lib := range libs {
353+ if err := core.PuregoSafeRegister(fptr, lib, name); err == nil {
354+ return
355+ }
356+ }
344357 }
358+
345359 {{range .Aliases -}}
346360 {{if .TypeGetter -}}
347- core.PuregoSafeRegister(&x{{.Name}}GLibType, lib, "{{.TypeGetter}}")
361+ tryRegister(&x{{.Name}}GLibType, "{{.TypeGetter}}")
348362 {{end}}
349363 {{end}}
350364 {{range .Enums -}}
351365 {{if .TypeGetter -}}
352- core.PuregoSafeRegister(&x{{.Name}}GLibType, lib, "{{.TypeGetter}}")
366+ tryRegister(&x{{.Name}}GLibType, "{{.TypeGetter}}")
353367 {{end}}
354368 {{end}}
355369 {{range .Functions -}}
356- core.PuregoSafeRegister(&x{{.Name}}, lib, "{{.CName}}")
370+ tryRegister(&x{{.Name}}, "{{.CName}}")
357371 {{end}}
358372
359373 {{range .Records -}}
360374 {{$rec := .}}
361375 {{if .TypeGetter -}}
362- core.PuregoSafeRegister(&x{{.Name}}GLibType, lib, "{{.TypeGetter}}")
376+ tryRegister(&x{{.Name}}GLibType, "{{.TypeGetter}}")
363377 {{end}}
364378 {{range .Constructors -}}
365- core.PuregoSafeRegister(&x{{.Name}}, lib, "{{.CName}}")
379+ tryRegister(&x{{.Name}}, "{{.CName}}")
366380 {{end}}
367381 {{range .Receivers -}}
368- core.PuregoSafeRegister(&x{{$rec.Name}}{{.Name}}, lib, "{{.CName}}")
382+ tryRegister(&x{{$rec.Name}}{{.Name}}, "{{.CName}}")
369383 {{end}}
370384 {{end}}
371385
372386 {{range .Classes -}}
373387 {{$cls := .}}
374388 {{if .TypeGetter -}}
375- core.PuregoSafeRegister(&x{{.Name}}GLibType, lib, "{{.TypeGetter}}")
389+ tryRegister(&x{{.Name}}GLibType, "{{.TypeGetter}}")
376390 {{end}}
377391 {{range .Constructors -}}
378- core.PuregoSafeRegister(&x{{.Name}}, lib, "{{.CName}}")
392+ tryRegister(&x{{.Name}}, "{{.CName}}")
379393 {{end}}
380394 {{range .Receivers -}}
381- core.PuregoSafeRegister(&x{{$cls.Name}}{{.Name}}, lib, "{{.CName}}")
395+ tryRegister(&x{{$cls.Name}}{{.Name}}, "{{.CName}}")
382396 {{end}}
383397 {{range .Functions -}}
384- core.PuregoSafeRegister(&x{{.Name}}, lib, "{{.CName}}")
398+ tryRegister(&x{{.Name}}, "{{.CName}}")
385399 {{end}}
386400 {{end}}
387401
388402 {{range .Interfaces -}}
389403 {{if .TypeGetter -}}
390- core.PuregoSafeRegister(&x{{.Name}}GLibType, lib, "{{.TypeGetter}}")
404+ tryRegister(&x{{.Name}}GLibType, "{{.TypeGetter}}")
391405 {{end}}
392406 {{range .Methods -}}
393- core.PuregoSafeRegister(&{{.Namespace}}X{{.FullName}}, lib, "{{.CName}}")
407+ tryRegister(&{{.Namespace}}X{{.FullName}}, "{{.CName}}")
394408 {{end}}
395409 {{end}}
396410 }
@@ -335,62 +335,76 @@ func init() {
335 {{if .PkgConfigName -}}335 {{if .PkgConfigName -}}
336 core.SetPackageName("{{.PkgEnv}}", "{{.PkgConfigName}}")336 core.SetPackageName("{{.PkgEnv}}", "{{.PkgConfigName}}")
337 {{end -}}337 {{end -}}
338- {{if .SharedLibrary -}}338+ {{if .SharedLibraries -}}
339- core.SetSharedLibrary("{{.PkgEnv}}", "{{.SharedLibrary}}")339+ core.SetSharedLibraries("{{.PkgEnv}}", []string{ {{range .SharedLibraries}}"{{.}}", {{end}} })
340 {{end -}}340 {{end -}}
341- lib, err := purego.Dlopen(core.GetPath("{{.PkgEnv}}"), purego.RTLD_NOW|purego.RTLD_GLOBAL)341+
342- if err != nil {342+ var libs []uintptr
343+ for _, libPath := range core.GetPaths("{{.PkgEnv}}") {
344+ lib, err := purego.Dlopen(libPath, purego.RTLD_NOW|purego.RTLD_GLOBAL)
345+ if err != nil {
343 panic(err)346 panic(err)
347+ }
348+ libs = append(libs, lib)
349+ }
350+
351+ tryRegister := func(fptr interface{}, name string) {
352+ for _, lib := range libs {
353+ if err := core.PuregoSafeRegister(fptr, lib, name); err == nil {
354+ return
355+ }
356+ }
344 }357 }
358+
345 {{range .Aliases -}}359 {{range .Aliases -}}
346 {{if .TypeGetter -}}360 {{if .TypeGetter -}}
347- core.PuregoSafeRegister(&x{{.Name}}GLibType, lib, "{{.TypeGetter}}")361+ tryRegister(&x{{.Name}}GLibType, "{{.TypeGetter}}")
348 {{end}}362 {{end}}
349 {{end}}363 {{end}}
350 {{range .Enums -}}364 {{range .Enums -}}
351 {{if .TypeGetter -}}365 {{if .TypeGetter -}}
352- core.PuregoSafeRegister(&x{{.Name}}GLibType, lib, "{{.TypeGetter}}")366+ tryRegister(&x{{.Name}}GLibType, "{{.TypeGetter}}")
353 {{end}}367 {{end}}
354 {{end}}368 {{end}}
355 {{range .Functions -}}369 {{range .Functions -}}
356- core.PuregoSafeRegister(&x{{.Name}}, lib, "{{.CName}}")370+ tryRegister(&x{{.Name}}, "{{.CName}}")
357 {{end}}371 {{end}}
358 372
359 {{range .Records -}}373 {{range .Records -}}
360 {{$rec := .}}374 {{$rec := .}}
361 {{if .TypeGetter -}}375 {{if .TypeGetter -}}
362- core.PuregoSafeRegister(&x{{.Name}}GLibType, lib, "{{.TypeGetter}}")376+ tryRegister(&x{{.Name}}GLibType, "{{.TypeGetter}}")
363 {{end}}377 {{end}}
364 {{range .Constructors -}}378 {{range .Constructors -}}
365- core.PuregoSafeRegister(&x{{.Name}}, lib, "{{.CName}}")379+ tryRegister(&x{{.Name}}, "{{.CName}}")
366 {{end}}380 {{end}}
367 {{range .Receivers -}}381 {{range .Receivers -}}
368- core.PuregoSafeRegister(&x{{$rec.Name}}{{.Name}}, lib, "{{.CName}}")382+ tryRegister(&x{{$rec.Name}}{{.Name}}, "{{.CName}}")
369 {{end}}383 {{end}}
370 {{end}}384 {{end}}
371 385
372 {{range .Classes -}}386 {{range .Classes -}}
373 {{$cls := .}}387 {{$cls := .}}
374 {{if .TypeGetter -}}388 {{if .TypeGetter -}}
375- core.PuregoSafeRegister(&x{{.Name}}GLibType, lib, "{{.TypeGetter}}")389+ tryRegister(&x{{.Name}}GLibType, "{{.TypeGetter}}")
376 {{end}}390 {{end}}
377 {{range .Constructors -}}391 {{range .Constructors -}}
378- core.PuregoSafeRegister(&x{{.Name}}, lib, "{{.CName}}")392+ tryRegister(&x{{.Name}}, "{{.CName}}")
379 {{end}}393 {{end}}
380 {{range .Receivers -}}394 {{range .Receivers -}}
381- core.PuregoSafeRegister(&x{{$cls.Name}}{{.Name}}, lib, "{{.CName}}")395+ tryRegister(&x{{$cls.Name}}{{.Name}}, "{{.CName}}")
382 {{end}}396 {{end}}
383 {{range .Functions -}}397 {{range .Functions -}}
384- core.PuregoSafeRegister(&x{{.Name}}, lib, "{{.CName}}")398+ tryRegister(&x{{.Name}}, "{{.CName}}")
385 {{end}}399 {{end}}
386 {{end}}400 {{end}}
387 401
388 {{range .Interfaces -}}402 {{range .Interfaces -}}
389 {{if .TypeGetter -}}403 {{if .TypeGetter -}}
390- core.PuregoSafeRegister(&x{{.Name}}GLibType, lib, "{{.TypeGetter}}")404+ tryRegister(&x{{.Name}}GLibType, "{{.TypeGetter}}")
391 {{end}}405 {{end}}
392 {{range .Methods -}}406 {{range .Methods -}}
393- core.PuregoSafeRegister(&{{.Namespace}}X{{.FullName}}, lib, "{{.CName}}")407+ tryRegister(&{{.Namespace}}X{{.FullName}}, "{{.CName}}")
394 {{end}}408 {{end}}
395 {{end}}409 {{end}}
396 }410 }

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 1

Tags 102

↑↓ moveenter openesc close