v2.16.0 ebd2ab3

push_test.go RawHistoryFolder

1package cli
2
3import (
4	"os"
5	"path/filepath"
6	"testing"
7
8	"github.com/stretchr/testify/assert"
9)
10
11func TestValidateBuildArg(t *testing.T) {
12	tests := []struct {
13		name    string
14		arg     string
15		wantErr bool
16	}{
17		{"valid simple", "FOO=bar", false},
18		{"valid with underscore key", "_FOO=bar", false},
19		{"valid with numbers in key", "FOO123=bar", false},
20		{"valid empty value", "FOO=", false},
21		{"valid complex value", "FOO=bar baz=qux", false},
22		{"invalid no equals", "FOO", true},
23		{"invalid starts with number", "1FOO=bar", true},
24		{"invalid starts with dash", "-FOO=bar", true},
25		{"invalid special chars in key", "FOO-BAR=baz", true},
26		{"invalid empty string", "", true},
27		{"invalid equals only", "=value", true},
28	}
29
30	for _, tt := range tests {
31		t.Run(tt.name, func(t *testing.T) {
32			err := validateBuildArg(tt.arg)
33			if tt.wantErr {
34				assert.Error(t, err)
35			} else {
36				assert.NoError(t, err)
37			}
38		})
39	}
40}
41
42func TestParseImageRef(t *testing.T) {
43	tests := []struct {
44		name         string
45		image        string
46		wantRegistry string
47		wantName     string
48		wantTag      string
49	}{
50		{"full ref", "reg.example.com/myapp:v1.0.0", "reg.example.com", "myapp", "v1.0.0"},
51		{"no tag", "reg.example.com/myapp", "reg.example.com", "myapp", "latest"},
52		{"latest tag", "reg.example.com/myapp:latest", "reg.example.com", "myapp", "latest"},
53		{"no slash", "myapp:v1.0.0", "", "", ""},
54	}
55
56	for _, tt := range tests {
57		t.Run(tt.name, func(t *testing.T) {
58			registry, name, tag := parseImageRef(tt.image)
59			assert.Equal(t, tt.wantRegistry, registry)
60			assert.Equal(t, tt.wantName, name)
61			assert.Equal(t, tt.wantTag, tag)
62		})
63	}
64}
65
66func TestBuildAndPush_BuildArgs(t *testing.T) {
67	// Verify buildImageArgs produces --load instead of --push
68	args := buildImageArgs("v1.0.0", "linux/amd64", "Dockerfile", []string{"CGO_ENABLED=0"}, "reg.example.com/app:v1.0.0", "reg.example.com/app:latest")
69
70	assert.Contains(t, args, "--load")
71	assert.NotContains(t, args, "--push")
72	assert.Contains(t, args, "--platform")
73	assert.Contains(t, args, "-f")
74	assert.Contains(t, args, "Dockerfile")
75	assert.Contains(t, args, "VERSION")
76	assert.NotContains(t, args, "VERSION=v1.0.0")
77}
78
79func TestParseTagRef(t *testing.T) {
80	tests := []struct {
81		name string
82		ref  string
83		want string
84	}{
85		{name: "github tag ref", ref: "refs/tags/v1.2.3", want: "v1.2.3"},
86		{name: "peeled tag ref", ref: "refs/tags/v1.2.3^{}", want: "v1.2.3"},
87		{name: "branch ref", ref: "refs/heads/main", want: ""},
88		{name: "empty", ref: "", want: ""},
89	}
90
91	for _, tt := range tests {
92		t.Run(tt.name, func(t *testing.T) {
93			assert.Equal(t, tt.want, parseTagRef(tt.ref))
94		})
95	}
96}
97
98func TestVersionFromTagRefs(t *testing.T) {
99	tests := []struct {
100		name string
101		env  map[string]string
102		want string
103	}{
104		{
105			name: "github ref tag",
106			env:  map[string]string{"GITHUB_REF": "refs/tags/v2.0.0"},
107			want: "v2.0.0",
108		},
109		{
110			name: "github ref type and name",
111			env: map[string]string{
112				"GITHUB_REF_TYPE": "tag",
113				"GITHUB_REF_NAME": "v2.1.0",
114			},
115			want: "v2.1.0",
116		},
117		{
118			name: "gitlab commit tag",
119			env:  map[string]string{"CI_COMMIT_TAG": "v3.0.0"},
120			want: "v3.0.0",
121		},
122		{
123			name: "azure source branch tag ref",
124			env:  map[string]string{"BUILD_SOURCEBRANCH": "refs/tags/v4.0.0"},
125			want: "v4.0.0",
126		},
127		{
128			name: "no tag refs",
129			env:  map[string]string{"GITHUB_REF": "refs/heads/main"},
130			want: "",
131		},
132	}
133
134	for _, tt := range tests {
135		t.Run(tt.name, func(t *testing.T) {
136			got := versionFromTagRefs(func(key string) string {
137				return tt.env[key]
138			})
139			assert.Equal(t, tt.want, got)
140		})
141	}
142}
143
144func TestBuildImageArgs_CustomDockerfile(t *testing.T) {
145	args := buildImageArgs("v1.0.0", "linux/amd64", "docker/app/Dockerfile", nil, "reg.example.com/app:v1.0.0", "reg.example.com/app:latest")
146
147	assert.Contains(t, args, "-f")
148	assert.Contains(t, args, "docker/app/Dockerfile")
149}
150
151func TestParseDockerfileLabels(t *testing.T) {
152	tests := []struct {
153		name     string
154		content  string
155		expected map[string]string
156	}{
157		{
158			name:    "simple label",
159			content: `LABEL gordon.domain="myapp.example.com"`,
160			expected: map[string]string{
161				"gordon.domain": "myapp.example.com",
162			},
163		},
164		{
165			name: "multiple labels",
166			content: `LABEL gordon.domain="myapp.example.com"
167LABEL gordon.port="8080"
168LABEL gordon.health="/health"`,
169			expected: map[string]string{
170				"gordon.domain": "myapp.example.com",
171				"gordon.port":   "8080",
172				"gordon.health": "/health",
173			},
174		},
175		{
176			name:    "multi-label on one line",
177			content: `LABEL gordon.domain="myapp.example.com" gordon.port="8080"`,
178			expected: map[string]string{
179				"gordon.domain": "myapp.example.com",
180				"gordon.port":   "8080",
181			},
182		},
183		{
184			name:    "unquoted value",
185			content: `LABEL gordon.domain=myapp.example.com`,
186			expected: map[string]string{
187				"gordon.domain": "myapp.example.com",
188			},
189		},
190		{
191			name:    "single-quoted value",
192			content: `LABEL gordon.domain='myapp.example.com'`,
193			expected: map[string]string{
194				"gordon.domain": "myapp.example.com",
195			},
196		},
197		{
198			name:    "non-gordon labels ignored",
199			content: `LABEL maintainer="John" gordon.domain="myapp.example.com"`,
200			expected: map[string]string{
201				"gordon.domain": "myapp.example.com",
202			},
203		},
204		{
205			name:    "comma-separated domains",
206			content: `LABEL gordon.domains="app1.example.com,app2.example.com"`,
207			expected: map[string]string{
208				"gordon.domains": "app1.example.com,app2.example.com",
209			},
210		},
211		{
212			name:     "no labels",
213			content:  `FROM alpine:latest`,
214			expected: map[string]string{},
215		},
216		{
217			name:     "comments and empty lines",
218			content:  "# This is a comment\n\nLABEL gordon.domain=\"test.com\"",
219			expected: map[string]string{"gordon.domain": "test.com"},
220		},
221	}
222
223	for _, tt := range tests {
224		t.Run(tt.name, func(t *testing.T) {
225			// Write a temp Dockerfile
226			dir := t.TempDir()
227			dockerfile := filepath.Join(dir, "Dockerfile")
228			err := os.WriteFile(dockerfile, []byte(tt.content), 0644)
229			assert.NoError(t, err)
230
231			labels := parseDockerfileLabels(dockerfile)
232			assert.Equal(t, tt.expected, labels)
233		})
234	}
235}
236
237func TestParseDockerfileLabels_NonExistent(t *testing.T) {
238	labels := parseDockerfileLabels("/nonexistent/Dockerfile")
239	assert.Empty(t, labels)
240}
241
242func TestDetectImageName_FromDockerfileLabels(t *testing.T) {
243	dir := t.TempDir()
244	dockerfile := filepath.Join(dir, "Dockerfile")
245	err := os.WriteFile(dockerfile, []byte(`FROM alpine
246LABEL gordon.domain="myapp.example.com"
247`), 0644)
248	assert.NoError(t, err)
249
250	name, err := detectImageName(dockerfile)
251	assert.NoError(t, err)
252	assert.Equal(t, "myapp.example.com", name)
253}
254
255func TestDetectImageName_FallbackToDir(t *testing.T) {
256	dir := t.TempDir()
257	dockerfile := filepath.Join(dir, "Dockerfile")
258	err := os.WriteFile(dockerfile, []byte(`FROM alpine
259`), 0644)
260	assert.NoError(t, err)
261
262	// When no gordon.domain label, falls back to cwd basename
263	name, err := detectImageName(dockerfile)
264	assert.NoError(t, err)
265	assert.NotEmpty(t, name)
266}
267
268func TestSplitLabelPairs(t *testing.T) {
269	tests := []struct {
270		name     string
271		content  string
272		expected []string
273	}{
274		{
275			name:     "single pair",
276			content:  `gordon.domain="test.com"`,
277			expected: []string{`gordon.domain="test.com"`},
278		},
279		{
280			name:     "two pairs",
281			content:  `gordon.domain="test.com" gordon.port="8080"`,
282			expected: []string{`gordon.domain="test.com"`, `gordon.port="8080"`},
283		},
284		{
285			name:     "unquoted",
286			content:  `gordon.domain=test.com gordon.port=8080`,
287			expected: []string{`gordon.domain=test.com`, `gordon.port=8080`},
288		},
289		{
290			name:     "quoted with spaces in value",
291			content:  `gordon.domain="my app.com"`,
292			expected: []string{`gordon.domain="my app.com"`},
293		},
294	}
295
296	for _, tt := range tests {
297		t.Run(tt.name, func(t *testing.T) {
298			result := splitLabelPairs(tt.content)
299			assert.Equal(t, tt.expected, result)
300		})
301	}
302}
303
304func TestParseLabelPair(t *testing.T) {
305	tests := []struct {
306		name      string
307		pair      string
308		wantKey   string
309		wantValue string
310		wantOk    bool
311	}{
312		{"quoted", `gordon.domain="test.com"`, "gordon.domain", "test.com", true},
313		{"unquoted", `gordon.domain=test.com`, "gordon.domain", "test.com", true},
314		{"single quoted", `gordon.domain='test.com'`, "gordon.domain", "test.com", true},
315		{"empty value", `gordon.domain=`, "gordon.domain", "", true},
316		{"no equals", `gordon.domain`, "", "", false},
317	}
318
319	for _, tt := range tests {
320		t.Run(tt.name, func(t *testing.T) {
321			key, value, ok := parseLabelPair(tt.pair)
322			assert.Equal(t, tt.wantOk, ok)
323			if ok {
324				assert.Equal(t, tt.wantKey, key)
325				assert.Equal(t, tt.wantValue, value)
326			}
327		})
328	}
329}

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