detached 641acf4

feat(runtime): add GetImageLabels and UntagImage interfaces

- Add GetImageLabels to ContainerRuntime for inspecting image labels
- Add UntagImage to remove tags without deleting image layers
- Add domain/labels.go with gordon.* label constants
- Implement both methods in docker adapter
- Regenerate mocks
commit
641acf4e81219f2b1ec75e0ca61dbbbf4834223e
author
brice · · 2026-01-14 06:23 UTC
parent
a6ac1fa
signature
GPG VerifiedCD88F9E564E0102646FB6077A1A07C7C7C3E349D · arch-gpg Verified for the committer email
4 files · +193 -0
M internal/adapters/out/docker/runtime.go +48 -0
@@ -88,6 +88,7 @@ func (r *Runtime) CreateContainer(ctx context.Context, config *domain.ContainerC
8888 // Create container configuration
8989 containerConfig := &container.Config{
9090 Image: config.Image,
91+ Hostname: config.Hostname,
9192 Env: config.Env,
9293 ExposedPorts: exposedPorts,
9394 WorkingDir: config.WorkingDir,
@@ -444,6 +445,29 @@ func (r *Runtime) TagImage(ctx context.Context, sourceRef, targetRef string) err
444445 return nil
445446 }
446447
448+// UntagImage removes a tag from an image without deleting the underlying image layers.
449+func (r *Runtime) UntagImage(ctx context.Context, imageRef string) error {
450+ ctx = zerowrap.CtxWithFields(ctx, map[string]any{
451+ zerowrap.FieldLayer: "adapter",
452+ zerowrap.FieldAdapter: "docker",
453+ zerowrap.FieldAction: "UntagImage",
454+ "image": imageRef,
455+ })
456+ log := zerowrap.FromCtx(ctx)
457+
458+ // ImageRemove with PruneChildren=false only removes the tag, not the image layers
459+ _, err := r.client.ImageRemove(ctx, imageRef, image.RemoveOptions{
460+ Force: false,
461+ PruneChildren: false,
462+ })
463+ if err != nil {
464+ return log.WrapErr(err, "failed to untag image")
465+ }
466+
467+ log.Debug().Msg("image untagged successfully")
468+ return nil
469+}
470+
447471 // RemoveImage removes an image.
448472 func (r *Runtime) RemoveImage(ctx context.Context, imageRef string, force bool) error {
449473 ctx = zerowrap.CtxWithFields(ctx, map[string]any{
@@ -816,6 +840,30 @@ func (r *Runtime) InspectImageEnv(ctx context.Context, imageRef string) ([]strin
816840 return envVars, nil
817841 }
818842
843+// GetImageLabels returns the labels defined on an image.
844+func (r *Runtime) GetImageLabels(ctx context.Context, imageRef string) (map[string]string, error) {
845+ ctx = zerowrap.CtxWithFields(ctx, map[string]any{
846+ zerowrap.FieldLayer: "adapter",
847+ zerowrap.FieldAdapter: "docker",
848+ zerowrap.FieldAction: "GetImageLabels",
849+ "image": imageRef,
850+ })
851+ log := zerowrap.FromCtx(ctx)
852+
853+ imageInspect, err := r.client.ImageInspect(ctx, imageRef)
854+ if err != nil {
855+ return nil, log.WrapErr(err, "failed to inspect image")
856+ }
857+
858+ labels := make(map[string]string)
859+ if imageInspect.Config != nil && imageInspect.Config.Labels != nil {
860+ labels = imageInspect.Config.Labels
861+ }
862+
863+ log.Debug().Int("label_count", len(labels)).Msg("retrieved image labels")
864+ return labels, nil
865+}
866+
819867 // CreateNetwork creates a new Docker network.
820868 func (r *Runtime) CreateNetwork(ctx context.Context, name string, options map[string]string) error {
821869 ctx = zerowrap.CtxWithFields(ctx, map[string]any{
@@ -88,6 +88,7 @@ func (r *Runtime) CreateContainer(ctx context.Context, config *domain.ContainerC
88 // Create container configuration88 // Create container configuration
89 containerConfig := &container.Config{89 containerConfig := &container.Config{
90 Image: config.Image,90 Image: config.Image,
91+ Hostname: config.Hostname,
91 Env: config.Env,92 Env: config.Env,
92 ExposedPorts: exposedPorts,93 ExposedPorts: exposedPorts,
93 WorkingDir: config.WorkingDir,94 WorkingDir: config.WorkingDir,
@@ -444,6 +445,29 @@ func (r *Runtime) TagImage(ctx context.Context, sourceRef, targetRef string) err
444 return nil445 return nil
445 }446 }
446 447
448+// UntagImage removes a tag from an image without deleting the underlying image layers.
449+func (r *Runtime) UntagImage(ctx context.Context, imageRef string) error {
450+ ctx = zerowrap.CtxWithFields(ctx, map[string]any{
451+ zerowrap.FieldLayer: "adapter",
452+ zerowrap.FieldAdapter: "docker",
453+ zerowrap.FieldAction: "UntagImage",
454+ "image": imageRef,
455+ })
456+ log := zerowrap.FromCtx(ctx)
457+
458+ // ImageRemove with PruneChildren=false only removes the tag, not the image layers
459+ _, err := r.client.ImageRemove(ctx, imageRef, image.RemoveOptions{
460+ Force: false,
461+ PruneChildren: false,
462+ })
463+ if err != nil {
464+ return log.WrapErr(err, "failed to untag image")
465+ }
466+
467+ log.Debug().Msg("image untagged successfully")
468+ return nil
469+}
470+
447 // RemoveImage removes an image.471 // RemoveImage removes an image.
448 func (r *Runtime) RemoveImage(ctx context.Context, imageRef string, force bool) error {472 func (r *Runtime) RemoveImage(ctx context.Context, imageRef string, force bool) error {
449 ctx = zerowrap.CtxWithFields(ctx, map[string]any{473 ctx = zerowrap.CtxWithFields(ctx, map[string]any{
@@ -816,6 +840,30 @@ func (r *Runtime) InspectImageEnv(ctx context.Context, imageRef string) ([]strin
816 return envVars, nil840 return envVars, nil
817 }841 }
818 842
843+// GetImageLabels returns the labels defined on an image.
844+func (r *Runtime) GetImageLabels(ctx context.Context, imageRef string) (map[string]string, error) {
845+ ctx = zerowrap.CtxWithFields(ctx, map[string]any{
846+ zerowrap.FieldLayer: "adapter",
847+ zerowrap.FieldAdapter: "docker",
848+ zerowrap.FieldAction: "GetImageLabels",
849+ "image": imageRef,
850+ })
851+ log := zerowrap.FromCtx(ctx)
852+
853+ imageInspect, err := r.client.ImageInspect(ctx, imageRef)
854+ if err != nil {
855+ return nil, log.WrapErr(err, "failed to inspect image")
856+ }
857+
858+ labels := make(map[string]string)
859+ if imageInspect.Config != nil && imageInspect.Config.Labels != nil {
860+ labels = imageInspect.Config.Labels
861+ }
862+
863+ log.Debug().Int("label_count", len(labels)).Msg("retrieved image labels")
864+ return labels, nil
865+}
866+
819 // CreateNetwork creates a new Docker network.867 // CreateNetwork creates a new Docker network.
820 func (r *Runtime) CreateNetwork(ctx context.Context, name string, options map[string]string) error {868 func (r *Runtime) CreateNetwork(ctx context.Context, name string, options map[string]string) error {
821 ctx = zerowrap.CtxWithFields(ctx, map[string]any{869 ctx = zerowrap.CtxWithFields(ctx, map[string]any{
M internal/boundaries/out/mocks/mock_container_runtime.go +125 -0
@@ -707,6 +707,74 @@ func (_c *MockContainerRuntime_GetImageExposedPorts_Call) RunAndReturn(run func(
707707 return _c
708708 }
709709
710+// GetImageLabels provides a mock function for the type MockContainerRuntime
711+func (_mock *MockContainerRuntime) GetImageLabels(ctx context.Context, imageRef string) (map[string]string, error) {
712+ ret := _mock.Called(ctx, imageRef)
713+
714+ if len(ret) == 0 {
715+ panic("no return value specified for GetImageLabels")
716+ }
717+
718+ var r0 map[string]string
719+ var r1 error
720+ if returnFunc, ok := ret.Get(0).(func(context.Context, string) (map[string]string, error)); ok {
721+ return returnFunc(ctx, imageRef)
722+ }
723+ if returnFunc, ok := ret.Get(0).(func(context.Context, string) map[string]string); ok {
724+ r0 = returnFunc(ctx, imageRef)
725+ } else {
726+ if ret.Get(0) != nil {
727+ r0 = ret.Get(0).(map[string]string)
728+ }
729+ }
730+ if returnFunc, ok := ret.Get(1).(func(context.Context, string) error); ok {
731+ r1 = returnFunc(ctx, imageRef)
732+ } else {
733+ r1 = ret.Error(1)
734+ }
735+ return r0, r1
736+}
737+
738+// MockContainerRuntime_GetImageLabels_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'GetImageLabels'
739+type MockContainerRuntime_GetImageLabels_Call struct {
740+ *mock.Call
741+}
742+
743+// GetImageLabels is a helper method to define mock.On call
744+// - ctx context.Context
745+// - imageRef string
746+func (_e *MockContainerRuntime_Expecter) GetImageLabels(ctx interface{}, imageRef interface{}) *MockContainerRuntime_GetImageLabels_Call {
747+ return &MockContainerRuntime_GetImageLabels_Call{Call: _e.mock.On("GetImageLabels", ctx, imageRef)}
748+}
749+
750+func (_c *MockContainerRuntime_GetImageLabels_Call) Run(run func(ctx context.Context, imageRef string)) *MockContainerRuntime_GetImageLabels_Call {
751+ _c.Call.Run(func(args mock.Arguments) {
752+ var arg0 context.Context
753+ if args[0] != nil {
754+ arg0 = args[0].(context.Context)
755+ }
756+ var arg1 string
757+ if args[1] != nil {
758+ arg1 = args[1].(string)
759+ }
760+ run(
761+ arg0,
762+ arg1,
763+ )
764+ })
765+ return _c
766+}
767+
768+func (_c *MockContainerRuntime_GetImageLabels_Call) Return(stringToString map[string]string, err error) *MockContainerRuntime_GetImageLabels_Call {
769+ _c.Call.Return(stringToString, err)
770+ return _c
771+}
772+
773+func (_c *MockContainerRuntime_GetImageLabels_Call) RunAndReturn(run func(ctx context.Context, imageRef string) (map[string]string, error)) *MockContainerRuntime_GetImageLabels_Call {
774+ _c.Call.Return(run)
775+ return _c
776+}
777+
710778 // InspectContainer provides a mock function for the type MockContainerRuntime
711779 func (_mock *MockContainerRuntime) InspectContainer(ctx context.Context, containerID string) (*domain.Container, error) {
712780 ret := _mock.Called(ctx, containerID)
@@ -1955,6 +2023,63 @@ func (_c *MockContainerRuntime_TagImage_Call) RunAndReturn(run func(ctx context.
19552023 return _c
19562024 }
19572025
2026+// UntagImage provides a mock function for the type MockContainerRuntime
2027+func (_mock *MockContainerRuntime) UntagImage(ctx context.Context, imageRef string) error {
2028+ ret := _mock.Called(ctx, imageRef)
2029+
2030+ if len(ret) == 0 {
2031+ panic("no return value specified for UntagImage")
2032+ }
2033+
2034+ var r0 error
2035+ if returnFunc, ok := ret.Get(0).(func(context.Context, string) error); ok {
2036+ r0 = returnFunc(ctx, imageRef)
2037+ } else {
2038+ r0 = ret.Error(0)
2039+ }
2040+ return r0
2041+}
2042+
2043+// MockContainerRuntime_UntagImage_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'UntagImage'
2044+type MockContainerRuntime_UntagImage_Call struct {
2045+ *mock.Call
2046+}
2047+
2048+// UntagImage is a helper method to define mock.On call
2049+// - ctx context.Context
2050+// - imageRef string
2051+func (_e *MockContainerRuntime_Expecter) UntagImage(ctx interface{}, imageRef interface{}) *MockContainerRuntime_UntagImage_Call {
2052+ return &MockContainerRuntime_UntagImage_Call{Call: _e.mock.On("UntagImage", ctx, imageRef)}
2053+}
2054+
2055+func (_c *MockContainerRuntime_UntagImage_Call) Run(run func(ctx context.Context, imageRef string)) *MockContainerRuntime_UntagImage_Call {
2056+ _c.Call.Run(func(args mock.Arguments) {
2057+ var arg0 context.Context
2058+ if args[0] != nil {
2059+ arg0 = args[0].(context.Context)
2060+ }
2061+ var arg1 string
2062+ if args[1] != nil {
2063+ arg1 = args[1].(string)
2064+ }
2065+ run(
2066+ arg0,
2067+ arg1,
2068+ )
2069+ })
2070+ return _c
2071+}
2072+
2073+func (_c *MockContainerRuntime_UntagImage_Call) Return(err error) *MockContainerRuntime_UntagImage_Call {
2074+ _c.Call.Return(err)
2075+ return _c
2076+}
2077+
2078+func (_c *MockContainerRuntime_UntagImage_Call) RunAndReturn(run func(ctx context.Context, imageRef string) error) *MockContainerRuntime_UntagImage_Call {
2079+ _c.Call.Return(run)
2080+ return _c
2081+}
2082+
19582083 // Version provides a mock function for the type MockContainerRuntime
19592084 func (_mock *MockContainerRuntime) Version(ctx context.Context) (string, error) {
19602085 ret := _mock.Called(ctx)
@@ -707,6 +707,74 @@ func (_c *MockContainerRuntime_GetImageExposedPorts_Call) RunAndReturn(run func(
707 return _c707 return _c
708 }708 }
709 709
710+// GetImageLabels provides a mock function for the type MockContainerRuntime
711+func (_mock *MockContainerRuntime) GetImageLabels(ctx context.Context, imageRef string) (map[string]string, error) {
712+ ret := _mock.Called(ctx, imageRef)
713+
714+ if len(ret) == 0 {
715+ panic("no return value specified for GetImageLabels")
716+ }
717+
718+ var r0 map[string]string
719+ var r1 error
720+ if returnFunc, ok := ret.Get(0).(func(context.Context, string) (map[string]string, error)); ok {
721+ return returnFunc(ctx, imageRef)
722+ }
723+ if returnFunc, ok := ret.Get(0).(func(context.Context, string) map[string]string); ok {
724+ r0 = returnFunc(ctx, imageRef)
725+ } else {
726+ if ret.Get(0) != nil {
727+ r0 = ret.Get(0).(map[string]string)
728+ }
729+ }
730+ if returnFunc, ok := ret.Get(1).(func(context.Context, string) error); ok {
731+ r1 = returnFunc(ctx, imageRef)
732+ } else {
733+ r1 = ret.Error(1)
734+ }
735+ return r0, r1
736+}
737+
738+// MockContainerRuntime_GetImageLabels_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'GetImageLabels'
739+type MockContainerRuntime_GetImageLabels_Call struct {
740+ *mock.Call
741+}
742+
743+// GetImageLabels is a helper method to define mock.On call
744+// - ctx context.Context
745+// - imageRef string
746+func (_e *MockContainerRuntime_Expecter) GetImageLabels(ctx interface{}, imageRef interface{}) *MockContainerRuntime_GetImageLabels_Call {
747+ return &MockContainerRuntime_GetImageLabels_Call{Call: _e.mock.On("GetImageLabels", ctx, imageRef)}
748+}
749+
750+func (_c *MockContainerRuntime_GetImageLabels_Call) Run(run func(ctx context.Context, imageRef string)) *MockContainerRuntime_GetImageLabels_Call {
751+ _c.Call.Run(func(args mock.Arguments) {
752+ var arg0 context.Context
753+ if args[0] != nil {
754+ arg0 = args[0].(context.Context)
755+ }
756+ var arg1 string
757+ if args[1] != nil {
758+ arg1 = args[1].(string)
759+ }
760+ run(
761+ arg0,
762+ arg1,
763+ )
764+ })
765+ return _c
766+}
767+
768+func (_c *MockContainerRuntime_GetImageLabels_Call) Return(stringToString map[string]string, err error) *MockContainerRuntime_GetImageLabels_Call {
769+ _c.Call.Return(stringToString, err)
770+ return _c
771+}
772+
773+func (_c *MockContainerRuntime_GetImageLabels_Call) RunAndReturn(run func(ctx context.Context, imageRef string) (map[string]string, error)) *MockContainerRuntime_GetImageLabels_Call {
774+ _c.Call.Return(run)
775+ return _c
776+}
777+
710 // InspectContainer provides a mock function for the type MockContainerRuntime778 // InspectContainer provides a mock function for the type MockContainerRuntime
711 func (_mock *MockContainerRuntime) InspectContainer(ctx context.Context, containerID string) (*domain.Container, error) {779 func (_mock *MockContainerRuntime) InspectContainer(ctx context.Context, containerID string) (*domain.Container, error) {
712 ret := _mock.Called(ctx, containerID)780 ret := _mock.Called(ctx, containerID)
@@ -1955,6 +2023,63 @@ func (_c *MockContainerRuntime_TagImage_Call) RunAndReturn(run func(ctx context.
1955 return _c2023 return _c
1956 }2024 }
1957 2025
2026+// UntagImage provides a mock function for the type MockContainerRuntime
2027+func (_mock *MockContainerRuntime) UntagImage(ctx context.Context, imageRef string) error {
2028+ ret := _mock.Called(ctx, imageRef)
2029+
2030+ if len(ret) == 0 {
2031+ panic("no return value specified for UntagImage")
2032+ }
2033+
2034+ var r0 error
2035+ if returnFunc, ok := ret.Get(0).(func(context.Context, string) error); ok {
2036+ r0 = returnFunc(ctx, imageRef)
2037+ } else {
2038+ r0 = ret.Error(0)
2039+ }
2040+ return r0
2041+}
2042+
2043+// MockContainerRuntime_UntagImage_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'UntagImage'
2044+type MockContainerRuntime_UntagImage_Call struct {
2045+ *mock.Call
2046+}
2047+
2048+// UntagImage is a helper method to define mock.On call
2049+// - ctx context.Context
2050+// - imageRef string
2051+func (_e *MockContainerRuntime_Expecter) UntagImage(ctx interface{}, imageRef interface{}) *MockContainerRuntime_UntagImage_Call {
2052+ return &MockContainerRuntime_UntagImage_Call{Call: _e.mock.On("UntagImage", ctx, imageRef)}
2053+}
2054+
2055+func (_c *MockContainerRuntime_UntagImage_Call) Run(run func(ctx context.Context, imageRef string)) *MockContainerRuntime_UntagImage_Call {
2056+ _c.Call.Run(func(args mock.Arguments) {
2057+ var arg0 context.Context
2058+ if args[0] != nil {
2059+ arg0 = args[0].(context.Context)
2060+ }
2061+ var arg1 string
2062+ if args[1] != nil {
2063+ arg1 = args[1].(string)
2064+ }
2065+ run(
2066+ arg0,
2067+ arg1,
2068+ )
2069+ })
2070+ return _c
2071+}
2072+
2073+func (_c *MockContainerRuntime_UntagImage_Call) Return(err error) *MockContainerRuntime_UntagImage_Call {
2074+ _c.Call.Return(err)
2075+ return _c
2076+}
2077+
2078+func (_c *MockContainerRuntime_UntagImage_Call) RunAndReturn(run func(ctx context.Context, imageRef string) error) *MockContainerRuntime_UntagImage_Call {
2079+ _c.Call.Return(run)
2080+ return _c
2081+}
2082+
1958 // Version provides a mock function for the type MockContainerRuntime2083 // Version provides a mock function for the type MockContainerRuntime
1959 func (_mock *MockContainerRuntime) Version(ctx context.Context) (string, error) {2084 func (_mock *MockContainerRuntime) Version(ctx context.Context) (string, error) {
1960 ret := _mock.Called(ctx)2085 ret := _mock.Called(ctx)
M internal/boundaries/out/runtime.go +4 -0
@@ -30,6 +30,7 @@ type ContainerRuntime interface {
3030 PullImage(ctx context.Context, image string) error
3131 PullImageWithAuth(ctx context.Context, image, username, password string) error
3232 TagImage(ctx context.Context, sourceRef, targetRef string) error
33+ UntagImage(ctx context.Context, imageRef string) error
3334 RemoveImage(ctx context.Context, image string, force bool) error
3435 ListImages(ctx context.Context) ([]string, error)
3536
@@ -55,6 +56,9 @@ type ContainerRuntime interface {
5556 // Environment inspection
5657 InspectImageEnv(ctx context.Context, imageRef string) ([]string, error)
5758
59+ // Label inspection
60+ GetImageLabels(ctx context.Context, imageRef string) (map[string]string, error)
61+
5862 // Network management
5963 CreateNetwork(ctx context.Context, name string, options map[string]string) error
6064 RemoveNetwork(ctx context.Context, name string) error
@@ -30,6 +30,7 @@ type ContainerRuntime interface {
30 PullImage(ctx context.Context, image string) error30 PullImage(ctx context.Context, image string) error
31 PullImageWithAuth(ctx context.Context, image, username, password string) error31 PullImageWithAuth(ctx context.Context, image, username, password string) error
32 TagImage(ctx context.Context, sourceRef, targetRef string) error32 TagImage(ctx context.Context, sourceRef, targetRef string) error
33+ UntagImage(ctx context.Context, imageRef string) error
33 RemoveImage(ctx context.Context, image string, force bool) error34 RemoveImage(ctx context.Context, image string, force bool) error
34 ListImages(ctx context.Context) ([]string, error)35 ListImages(ctx context.Context) ([]string, error)
35 36
@@ -55,6 +56,9 @@ type ContainerRuntime interface {
55 // Environment inspection56 // Environment inspection
56 InspectImageEnv(ctx context.Context, imageRef string) ([]string, error)57 InspectImageEnv(ctx context.Context, imageRef string) ([]string, error)
57 58
59+ // Label inspection
60+ GetImageLabels(ctx context.Context, imageRef string) (map[string]string, error)
61+
58 // Network management62 // Network management
59 CreateNetwork(ctx context.Context, name string, options map[string]string) error63 CreateNetwork(ctx context.Context, name string, options map[string]string) error
60 RemoveNetwork(ctx context.Context, name string) error64 RemoveNetwork(ctx context.Context, name string) error
A internal/domain/labels.go +16 -0
@@ -0,0 +1,16 @@
1+package domain
2+
3+// Label keys used by Gordon for container and image metadata.
4+const (
5+ // Container labels
6+ LabelDomain = "gordon.domain"
7+ LabelImage = "gordon.image"
8+ LabelManaged = "gordon.managed"
9+ LabelRoute = "gordon.route"
10+ LabelAttachment = "gordon.attachment"
11+ LabelAttachedTo = "gordon.attached-to"
12+ LabelCreated = "gordon.created"
13+
14+ // Image labels (set in Dockerfile)
15+ LabelProxyPort = "gordon.proxy.port"
16+)
@@ -0,0 +1,16 @@
1+package domain
2+
3+// Label keys used by Gordon for container and image metadata.
4+const (
5+ // Container labels
6+ LabelDomain = "gordon.domain"
7+ LabelImage = "gordon.image"
8+ LabelManaged = "gordon.managed"
9+ LabelRoute = "gordon.route"
10+ LabelAttachment = "gordon.attachment"
11+ LabelAttachedTo = "gordon.attached-to"
12+ LabelCreated = "gordon.created"
13+
14+ // Image labels (set in Dockerfile)
15+ LabelProxyPort = "gordon.proxy.port"
16+)

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