v2.0.0 ae3e752

server_test.go RawHistoryFolder

1package registry
2
3import (
4	"bytes"
5	"context"
6	"fmt"
7	"net/http"
8	"net/http/httptest"
9	"os"
10	"strings"
11	"testing"
12
13	"github.com/stretchr/testify/assert"
14	"github.com/stretchr/testify/mock"
15	"github.com/stretchr/testify/require"
16
17	"gordon/internal/config"
18	"gordon/internal/events"
19	registryMocks "gordon/internal/registry/mocks"
20)
21
22// MockEventBus is a mock implementation of events.EventBus
23type MockEventBus struct {
24	mock.Mock
25}
26
27func (m *MockEventBus) Publish(eventType events.EventType, payload interface{}) error {
28	args := m.Called(eventType, payload)
29	return args.Error(0)
30}
31
32func (m *MockEventBus) Subscribe(handler events.EventHandler) error {
33	args := m.Called(handler)
34	return args.Error(0)
35}
36
37func (m *MockEventBus) Unsubscribe(handler events.EventHandler) error {
38	args := m.Called(handler)
39	return args.Error(0)
40}
41
42func (m *MockEventBus) Start() error {
43	args := m.Called()
44	return args.Error(0)
45}
46
47func (m *MockEventBus) Stop() error {
48	args := m.Called()
49	return args.Error(0)
50}
51
52func TestNewServer(t *testing.T) {
53	mockEventBus := &MockEventBus{}
54	cfg := &config.Config{
55		Server: config.ServerConfig{
56			DataDir: t.TempDir(),
57		},
58	}
59
60	server, err := NewServer(cfg, mockEventBus)
61
62	require.NoError(t, err)
63	assert.NotNil(t, server)
64	assert.Equal(t, cfg, server.config)
65	assert.Equal(t, mockEventBus, server.eventBus)
66	assert.NotNil(t, server.storage)
67	assert.NotNil(t, server.mux)
68}
69
70func TestNewServer_StorageInitError(t *testing.T) {
71	mockEventBus := &MockEventBus{}
72	cfg := &config.Config{
73		Server: config.ServerConfig{
74			DataDir: "/invalid/path/that/cannot/be/created",
75		},
76	}
77
78	server, err := NewServer(cfg, mockEventBus)
79
80	assert.Error(t, err)
81	assert.Nil(t, server)
82	assert.Contains(t, err.Error(), "failed to initialize storage")
83}
84
85func TestServer_HandleBase(t *testing.T) {
86	server := createTestServer(t)
87
88	req := httptest.NewRequest("GET", "/v2/", nil)
89	w := httptest.NewRecorder()
90
91	server.handleBase(w, req)
92
93	assert.Equal(t, http.StatusOK, w.Code)
94	assert.Equal(t, "registry/2.0", w.Header().Get("Docker-Distribution-API-Version"))
95}
96
97func TestServer_HandleGetManifest_Success(t *testing.T) {
98	server := createTestServerWithMockStorage(t)
99	mockStorage := server.storage.(*registryMocks.MockStorage)
100
101	name := "myapp"
102	reference := "latest"
103	manifestData := []byte(`{"test": "manifest"}`)
104	contentType := "application/vnd.docker.distribution.manifest.v2+json"
105
106	mockStorage.On("GetManifest", name, reference).Return(manifestData, contentType, nil)
107
108	req := httptest.NewRequest("GET", fmt.Sprintf("/v2/%s/manifests/%s", name, reference), nil)
109	req.SetPathValue("name", name)
110	req.SetPathValue("reference", reference)
111	w := httptest.NewRecorder()
112
113	server.handleGetManifest(w, req)
114
115	assert.Equal(t, http.StatusOK, w.Code)
116	assert.Equal(t, contentType, w.Header().Get("Content-Type"))
117	assert.Equal(t, fmt.Sprintf("%d", len(manifestData)), w.Header().Get("Content-Length"))
118	assert.Equal(t, manifestData, w.Body.Bytes())
119	mockStorage.AssertExpectations(t)
120}
121
122func TestServer_HandleGetManifest_HEAD(t *testing.T) {
123	server := createTestServerWithMockStorage(t)
124	mockStorage := server.storage.(*registryMocks.MockStorage)
125
126	name := "myapp"
127	reference := "latest"
128	manifestData := []byte(`{"test": "manifest"}`)
129	contentType := "application/vnd.docker.distribution.manifest.v2+json"
130
131	mockStorage.On("GetManifest", name, reference).Return(manifestData, contentType, nil)
132
133	req := httptest.NewRequest("HEAD", fmt.Sprintf("/v2/%s/manifests/%s", name, reference), nil)
134	req.SetPathValue("name", name)
135	req.SetPathValue("reference", reference)
136	w := httptest.NewRecorder()
137
138	server.handleGetManifest(w, req)
139
140	assert.Equal(t, http.StatusOK, w.Code)
141	assert.Equal(t, contentType, w.Header().Get("Content-Type"))
142	assert.Equal(t, fmt.Sprintf("%d", len(manifestData)), w.Header().Get("Content-Length"))
143	assert.Empty(t, w.Body.Bytes()) // HEAD should not return body
144	mockStorage.AssertExpectations(t)
145}
146
147func TestServer_HandleGetManifest_NotFound(t *testing.T) {
148	server := createTestServerWithMockStorage(t)
149	mockStorage := server.storage.(*registryMocks.MockStorage)
150
151	name := "nonexistent"
152	reference := "latest"
153
154	mockStorage.On("GetManifest", name, reference).Return(nil, "", fmt.Errorf("manifest not found"))
155
156	req := httptest.NewRequest("GET", fmt.Sprintf("/v2/%s/manifests/%s", name, reference), nil)
157	req.SetPathValue("name", name)
158	req.SetPathValue("reference", reference)
159	w := httptest.NewRecorder()
160
161	server.handleGetManifest(w, req)
162
163	assert.Equal(t, http.StatusNotFound, w.Code)
164	mockStorage.AssertExpectations(t)
165}
166
167func TestServer_HandlePutManifest_Success(t *testing.T) {
168	server := createTestServerWithMockStorage(t)
169	mockStorage := server.storage.(*registryMocks.MockStorage)
170	mockEventBus := server.eventBus.(*MockEventBus)
171
172	name := "myapp"
173	reference := "latest"
174	manifestData := []byte(`{"test": "manifest"}`)
175	contentType := "application/vnd.docker.distribution.manifest.v2+json"
176
177	mockStorage.On("PutManifest", name, reference, contentType, manifestData).Return(nil)
178	mockEventBus.On("Publish", events.ImagePushed, mock.MatchedBy(func(payload events.ImagePushedPayload) bool {
179		return payload.Name == name && payload.Reference == reference && bytes.Equal(payload.Manifest, manifestData)
180	})).Return(nil)
181
182	req := httptest.NewRequest("PUT", fmt.Sprintf("/v2/%s/manifests/%s", name, reference), bytes.NewReader(manifestData))
183	req.Header.Set("Content-Type", contentType)
184	req.SetPathValue("name", name)
185	req.SetPathValue("reference", reference)
186	w := httptest.NewRecorder()
187
188	server.handlePutManifest(w, req)
189
190	assert.Equal(t, http.StatusCreated, w.Code)
191	assert.Contains(t, w.Header().Get("Docker-Content-Digest"), "sha256:")
192	assert.Equal(t, fmt.Sprintf("/v2/%s/manifests/%s", name, reference), w.Header().Get("Location"))
193	mockStorage.AssertExpectations(t)
194	mockEventBus.AssertExpectations(t)
195}
196
197func TestServer_HandlePutManifest_NoContentType(t *testing.T) {
198	server := createTestServerWithMockStorage(t)
199
200	name := "myapp"
201	reference := "latest"
202	manifestData := []byte(`{"test": "manifest"}`)
203
204	req := httptest.NewRequest("PUT", fmt.Sprintf("/v2/%s/manifests/%s", name, reference), bytes.NewReader(manifestData))
205	// No Content-Type header
206	req.SetPathValue("name", name)
207	req.SetPathValue("reference", reference)
208	w := httptest.NewRecorder()
209
210	server.handlePutManifest(w, req)
211
212	assert.Equal(t, http.StatusBadRequest, w.Code)
213	assert.Contains(t, w.Body.String(), "Content-Type header required")
214}
215
216func TestServer_HandlePutManifest_StorageError(t *testing.T) {
217	server := createTestServerWithMockStorage(t)
218	mockStorage := server.storage.(*registryMocks.MockStorage)
219
220	name := "myapp"
221	reference := "latest"
222	manifestData := []byte(`{"test": "manifest"}`)
223	contentType := "application/vnd.docker.distribution.manifest.v2+json"
224
225	mockStorage.On("PutManifest", name, reference, contentType, manifestData).Return(fmt.Errorf("storage error"))
226
227	req := httptest.NewRequest("PUT", fmt.Sprintf("/v2/%s/manifests/%s", name, reference), bytes.NewReader(manifestData))
228	req.Header.Set("Content-Type", contentType)
229	req.SetPathValue("name", name)
230	req.SetPathValue("reference", reference)
231	w := httptest.NewRecorder()
232
233	server.handlePutManifest(w, req)
234
235	assert.Equal(t, http.StatusInternalServerError, w.Code)
236	mockStorage.AssertExpectations(t)
237}
238
239func TestServer_HandlePutManifest_WithAnnotations(t *testing.T) {
240	server := createTestServerWithMockStorage(t)
241	mockStorage := server.storage.(*registryMocks.MockStorage)
242	mockEventBus := server.eventBus.(*MockEventBus)
243
244	name := "myapp"
245	reference := "latest"
246	// OCI manifest with annotations
247	manifestData := []byte(`{
248		"schemaVersion": 2,
249		"mediaType": "application/vnd.oci.image.manifest.v1+json",
250		"annotations": {
251			"version": "v1.0",
252			"description": "Test image"
253		}
254	}`)
255	contentType := "application/vnd.oci.image.manifest.v1+json"
256
257	mockStorage.On("PutManifest", name, reference, contentType, manifestData).Return(nil)
258	mockEventBus.On("Publish", events.ImagePushed, mock.MatchedBy(func(payload events.ImagePushedPayload) bool {
259		return payload.Name == name && 
260			payload.Reference == reference && 
261			len(payload.Annotations) == 2 &&
262			payload.Annotations["version"] == "v1.0"
263	})).Return(nil)
264
265	req := httptest.NewRequest("PUT", fmt.Sprintf("/v2/%s/manifests/%s", name, reference), bytes.NewReader(manifestData))
266	req.Header.Set("Content-Type", contentType)
267	req.SetPathValue("name", name)
268	req.SetPathValue("reference", reference)
269	w := httptest.NewRecorder()
270
271	server.handlePutManifest(w, req)
272
273	assert.Equal(t, http.StatusCreated, w.Code)
274	mockStorage.AssertExpectations(t)
275	mockEventBus.AssertExpectations(t)
276}
277
278func TestServer_HandleGetBlob_Success(t *testing.T) {
279	server := createTestServerWithMockStorage(t)
280	mockStorage := server.storage.(*registryMocks.MockStorage)
281
282	name := "myapp"
283	digest := "sha256:abc123"
284	blobPath := "/tmp/test-blob"
285
286	// Create test blob file
287	testContent := "test blob content"
288	err := os.WriteFile(blobPath, []byte(testContent), 0644)
289	require.NoError(t, err)
290	defer os.Remove(blobPath)
291
292	mockStorage.On("GetBlobPath", digest).Return(blobPath, nil)
293
294	req := httptest.NewRequest("GET", fmt.Sprintf("/v2/%s/blobs/%s", name, digest), nil)
295	req.SetPathValue("name", name)
296	req.SetPathValue("digest", digest)
297	w := httptest.NewRecorder()
298
299	server.handleGetBlob(w, req)
300
301	assert.Equal(t, http.StatusOK, w.Code)
302	assert.Equal(t, testContent, w.Body.String())
303	mockStorage.AssertExpectations(t)
304}
305
306func TestServer_HandleGetBlob_NotFound(t *testing.T) {
307	server := createTestServerWithMockStorage(t)
308	mockStorage := server.storage.(*registryMocks.MockStorage)
309
310	name := "myapp"
311	digest := "sha256:nonexistent"
312
313	mockStorage.On("GetBlobPath", digest).Return("", fmt.Errorf("blob not found"))
314
315	req := httptest.NewRequest("GET", fmt.Sprintf("/v2/%s/blobs/%s", name, digest), nil)
316	req.SetPathValue("name", name)
317	req.SetPathValue("digest", digest)
318	w := httptest.NewRecorder()
319
320	server.handleGetBlob(w, req)
321
322	assert.Equal(t, http.StatusNotFound, w.Code)
323	mockStorage.AssertExpectations(t)
324}
325
326func TestServer_HandleStartBlobUpload_Success(t *testing.T) {
327	server := createTestServerWithMockStorage(t)
328	mockStorage := server.storage.(*registryMocks.MockStorage)
329
330	name := "myapp"
331	uuid := "test-upload-uuid"
332
333	mockStorage.On("StartBlobUpload", name).Return(uuid, nil)
334
335	req := httptest.NewRequest("POST", fmt.Sprintf("/v2/%s/blobs/uploads/", name), nil)
336	req.SetPathValue("name", name)
337	w := httptest.NewRecorder()
338
339	server.handleStartBlobUpload(w, req)
340
341	assert.Equal(t, http.StatusAccepted, w.Code)
342	assert.Equal(t, fmt.Sprintf("/v2/%s/blobs/uploads/%s", name, uuid), w.Header().Get("Location"))
343	assert.Equal(t, "0-0", w.Header().Get("Range"))
344	mockStorage.AssertExpectations(t)
345}
346
347func TestServer_HandleStartBlobUpload_Error(t *testing.T) {
348	server := createTestServerWithMockStorage(t)
349	mockStorage := server.storage.(*registryMocks.MockStorage)
350
351	name := "myapp"
352
353	mockStorage.On("StartBlobUpload", name).Return("", fmt.Errorf("storage error"))
354
355	req := httptest.NewRequest("POST", fmt.Sprintf("/v2/%s/blobs/uploads/", name), nil)
356	req.SetPathValue("name", name)
357	w := httptest.NewRecorder()
358
359	server.handleStartBlobUpload(w, req)
360
361	assert.Equal(t, http.StatusInternalServerError, w.Code)
362	mockStorage.AssertExpectations(t)
363}
364
365func TestServer_HandleBlobUpload_PATCH(t *testing.T) {
366	server := createTestServerWithMockStorage(t)
367	mockStorage := server.storage.(*registryMocks.MockStorage)
368
369	name := "myapp"
370	uuid := "test-upload-uuid"
371	chunk := []byte("chunk data")
372
373	mockStorage.On("AppendBlobChunk", name, uuid, chunk).Return(int64(len(chunk)), nil)
374
375	req := httptest.NewRequest("PATCH", fmt.Sprintf("/v2/%s/blobs/uploads/%s", name, uuid), bytes.NewReader(chunk))
376	req.SetPathValue("name", name)
377	req.SetPathValue("uuid", uuid)
378	w := httptest.NewRecorder()
379
380	server.handleBlobUpload(w, req)
381
382	assert.Equal(t, http.StatusAccepted, w.Code)
383	assert.Equal(t, fmt.Sprintf("/v2/%s/blobs/uploads/%s", name, uuid), w.Header().Get("Location"))
384	assert.Equal(t, fmt.Sprintf("0-%d", len(chunk)-1), w.Header().Get("Range"))
385	mockStorage.AssertExpectations(t)
386}
387
388func TestServer_HandleBlobUpload_PUT_Success(t *testing.T) {
389	server := createTestServerWithMockStorage(t)
390	mockStorage := server.storage.(*registryMocks.MockStorage)
391
392	name := "myapp"
393	uuid := "test-upload-uuid"
394	digest := "sha256:abc123"
395	chunk := []byte("final chunk")
396
397	mockStorage.On("AppendBlobChunk", name, uuid, chunk).Return(int64(len(chunk)), nil)
398	mockStorage.On("FinishBlobUpload", uuid, digest).Return(nil)
399
400	req := httptest.NewRequest("PUT", fmt.Sprintf("/v2/%s/blobs/uploads/%s?digest=%s", name, uuid, digest), bytes.NewReader(chunk))
401	req.SetPathValue("name", name)
402	req.SetPathValue("uuid", uuid)
403	w := httptest.NewRecorder()
404
405	server.handleBlobUpload(w, req)
406
407	assert.Equal(t, http.StatusCreated, w.Code)
408	assert.Equal(t, fmt.Sprintf("/v2/%s/blobs/%s", name, digest), w.Header().Get("Location"))
409	assert.Equal(t, digest, w.Header().Get("Docker-Content-Digest"))
410	mockStorage.AssertExpectations(t)
411}
412
413func TestServer_HandleBlobUpload_PUT_FinishError(t *testing.T) {
414	server := createTestServerWithMockStorage(t)
415	mockStorage := server.storage.(*registryMocks.MockStorage)
416
417	name := "myapp"
418	uuid := "test-upload-uuid"
419	digest := "sha256:abc123"
420	chunk := []byte("final chunk")
421
422	mockStorage.On("AppendBlobChunk", name, uuid, chunk).Return(int64(len(chunk)), nil)
423	mockStorage.On("FinishBlobUpload", uuid, digest).Return(fmt.Errorf("digest mismatch"))
424	mockStorage.On("CancelBlobUpload", uuid).Return(nil)
425
426	req := httptest.NewRequest("PUT", fmt.Sprintf("/v2/%s/blobs/uploads/%s?digest=%s", name, uuid, digest), bytes.NewReader(chunk))
427	req.SetPathValue("name", name)
428	req.SetPathValue("uuid", uuid)
429	w := httptest.NewRecorder()
430
431	server.handleBlobUpload(w, req)
432
433	assert.Equal(t, http.StatusBadRequest, w.Code)
434	assert.Contains(t, w.Body.String(), "digest mismatch")
435	mockStorage.AssertExpectations(t)
436}
437
438func TestServer_HandleBlobUpload_AppendError(t *testing.T) {
439	server := createTestServerWithMockStorage(t)
440	mockStorage := server.storage.(*registryMocks.MockStorage)
441
442	name := "myapp"
443	uuid := "test-upload-uuid"
444	chunk := []byte("chunk data")
445
446	mockStorage.On("AppendBlobChunk", name, uuid, chunk).Return(int64(0), fmt.Errorf("append error"))
447
448	req := httptest.NewRequest("PATCH", fmt.Sprintf("/v2/%s/blobs/uploads/%s", name, uuid), bytes.NewReader(chunk))
449	req.SetPathValue("name", name)
450	req.SetPathValue("uuid", uuid)
451	w := httptest.NewRecorder()
452
453	server.handleBlobUpload(w, req)
454
455	assert.Equal(t, http.StatusInternalServerError, w.Code)
456	mockStorage.AssertExpectations(t)
457}
458
459func TestServer_HandleListTags_Success(t *testing.T) {
460	server := createTestServerWithMockStorage(t)
461	mockStorage := server.storage.(*registryMocks.MockStorage)
462
463	name := "myapp"
464	tags := []string{"latest", "v1.0", "stable"}
465
466	mockStorage.On("ListTags", name).Return(tags, nil)
467
468	req := httptest.NewRequest("GET", fmt.Sprintf("/v2/%s/tags/list", name), nil)
469	req.SetPathValue("name", name)
470	w := httptest.NewRecorder()
471
472	server.handleListTags(w, req)
473
474	assert.Equal(t, http.StatusOK, w.Code)
475	assert.Equal(t, "application/json", w.Header().Get("Content-Type"))
476	
477	// The server correctly uses JSON marshaling for tags
478	expectedResponse := fmt.Sprintf(`{"name":"%s","tags":["latest","v1.0","stable"]}`, name)
479	assert.Equal(t, expectedResponse, w.Body.String())
480	mockStorage.AssertExpectations(t)
481}
482
483func TestServer_HandleListTags_NotFound(t *testing.T) {
484	server := createTestServerWithMockStorage(t)
485	mockStorage := server.storage.(*registryMocks.MockStorage)
486
487	name := "nonexistent"
488
489	mockStorage.On("ListTags", name).Return(nil, fmt.Errorf("not found"))
490
491	req := httptest.NewRequest("GET", fmt.Sprintf("/v2/%s/tags/list", name), nil)
492	req.SetPathValue("name", name)
493	w := httptest.NewRecorder()
494
495	server.handleListTags(w, req)
496
497	assert.Equal(t, http.StatusNotFound, w.Code)
498	mockStorage.AssertExpectations(t)
499}
500
501func TestServer_Start_Integration(t *testing.T) {
502	// This is more of an integration test to verify server startup
503	server := createTestServer(t)
504	server.config.Server.RegistryPort = 0 // Use any available port
505
506	ctx, cancel := context.WithCancel(context.Background())
507	defer cancel()
508
509	// Start server in background
510	errChan := make(chan error, 1)
511	go func() {
512		errChan <- server.Start(ctx)
513	}()
514
515	// Cancel context to shutdown server
516	cancel()
517
518	// Wait for server to shutdown
519	select {
520	case err := <-errChan:
521		assert.NoError(t, err)
522	}
523}
524
525func TestServer_SetupRoutes(t *testing.T) {
526	server := createTestServer(t)
527
528	// Test that routes are properly set up by checking a few key endpoints
529	testCases := []struct {
530		method       string
531		path         string
532		expectedCode int
533	}{
534		{"GET", "/v2/", http.StatusOK},
535		{"GET", "/v2/myapp/manifests/latest", http.StatusNotFound}, // Expected since manifest doesn't exist
536		{"HEAD", "/v2/myapp/manifests/latest", http.StatusNotFound}, // Expected since manifest doesn't exist
537		{"PUT", "/v2/myapp/manifests/latest", http.StatusBadRequest}, // Expected since no Content-Type header
538		{"GET", "/v2/myapp/blobs/sha256:abc123", http.StatusNotFound}, // Expected since blob doesn't exist
539		{"HEAD", "/v2/myapp/blobs/sha256:abc123", http.StatusNotFound}, // Expected since blob doesn't exist
540		{"POST", "/v2/myapp/blobs/uploads/", http.StatusAccepted},
541		{"PATCH", "/v2/myapp/blobs/uploads/uuid123", http.StatusInternalServerError}, // Expected since upload doesn't exist
542		{"PUT", "/v2/myapp/blobs/uploads/uuid123", http.StatusInternalServerError}, // Expected since upload doesn't exist
543		{"GET", "/v2/myapp/tags/list", http.StatusOK}, // Returns empty list when no tags exist
544	}
545
546	for _, tc := range testCases {
547		t.Run(fmt.Sprintf("%s %s", tc.method, tc.path), func(t *testing.T) {
548			req := httptest.NewRequest(tc.method, tc.path, nil)
549			w := httptest.NewRecorder()
550
551			// Set path values for routes that need them
552			if strings.Contains(tc.path, "myapp") {
553				req.SetPathValue("name", "myapp")
554			}
555			if strings.Contains(tc.path, "latest") {
556				req.SetPathValue("reference", "latest")
557			}
558			if strings.Contains(tc.path, "sha256:abc123") {
559				req.SetPathValue("digest", "sha256:abc123")
560			}
561			if strings.Contains(tc.path, "uuid123") {
562				req.SetPathValue("uuid", "uuid123")
563			}
564
565			server.mux.ServeHTTP(w, req)
566
567			// We verify that the route exists and returns the expected status
568			assert.Equal(t, tc.expectedCode, w.Code, "Route %s %s should return %d", tc.method, tc.path, tc.expectedCode)
569		})
570	}
571}
572
573// Helper functions
574
575func createTestServer(t *testing.T) *Server {
576	mockEventBus := &MockEventBus{}
577	cfg := &config.Config{
578		Server: config.ServerConfig{
579			DataDir:      t.TempDir(),
580			RegistryPort: 5000,
581		},
582		RegistryAuth: config.RegistryAuthConfig{
583			Enabled: false,
584		},
585	}
586
587	server, err := NewServer(cfg, mockEventBus)
588	require.NoError(t, err)
589	return server
590}
591
592func createTestServerWithMockStorage(t *testing.T) *Server {
593	mockEventBus := &MockEventBus{}
594	mockStorage := &registryMocks.MockStorage{}
595	
596	cfg := &config.Config{
597		Server: config.ServerConfig{
598			DataDir:      t.TempDir(),
599			RegistryPort: 5000,
600		},
601		RegistryAuth: config.RegistryAuthConfig{
602			Enabled: false,
603		},
604	}
605
606	server := &Server{
607		config:   cfg,
608		mux:      http.NewServeMux(),
609		storage:  mockStorage,
610		eventBus: mockEventBus,
611	}
612	server.setupRoutes()
613	return server
614}

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