1// Copyright 2017 The Gitea Authors. All rights reserved.
2// SPDX-License-Identifier: MIT
3
4package git
5
6import (
7 "path/filepath"
8 "slices"
9 "strings"
10 "testing"
11
12 "github.com/stretchr/testify/assert"
13 "github.com/stretchr/testify/require"
14)
15
16func TestCommitsCount(t *testing.T) {
17 bareRepo1Path := filepath.Join(testReposDir, "repo1_bare")
18
19 commitsCount, err := CommitsCount(DefaultContext,
20 CommitsCountOptions{
21 RepoPath: bareRepo1Path,
22 Revision: []string{"8006ff9adbf0cb94da7dad9e537e53817f9fa5c0"},
23 })
24
25 require.NoError(t, err)
26 assert.Equal(t, int64(3), commitsCount)
27}
28
29func TestCommitsCountWithoutBase(t *testing.T) {
30 bareRepo1Path := filepath.Join(testReposDir, "repo1_bare")
31
32 commitsCount, err := CommitsCount(DefaultContext,
33 CommitsCountOptions{
34 RepoPath: bareRepo1Path,
35 Not: "master",
36 Revision: []string{"branch1"},
37 })
38
39 require.NoError(t, err)
40 assert.Equal(t, int64(2), commitsCount)
41}
42
43func TestGetFullCommitID(t *testing.T) {
44 bareRepo1Path := filepath.Join(testReposDir, "repo1_bare")
45
46 id, err := GetFullCommitID(DefaultContext, bareRepo1Path, "8006ff9a")
47 require.NoError(t, err)
48 assert.Equal(t, "8006ff9adbf0cb94da7dad9e537e53817f9fa5c0", id)
49}
50
51func TestGetFullCommitIDError(t *testing.T) {
52 bareRepo1Path := filepath.Join(testReposDir, "repo1_bare")
53
54 id, err := GetFullCommitID(DefaultContext, bareRepo1Path, "unknown")
55 assert.Empty(t, id)
56 if assert.Error(t, err) {
57 assert.EqualError(t, err, "object does not exist [id: unknown, rel_path: ]")
58 }
59}
60
61func TestCommitFromReader(t *testing.T) {
62 commitString := `feaf4ba6bc635fec442f46ddd4512416ec43c2c2 commit 1074
63tree f1a6cb52b2d16773290cefe49ad0684b50a4f930
64parent 37991dec2c8e592043f47155ce4808d4580f9123
65author silverwind <me@silverwind.io> 1563741793 +0200
66committer silverwind <me@silverwind.io> 1563741793 +0200
67gpgsig -----BEGIN PGP SIGNATURE-----
68` + " " + `
69 iQIzBAABCAAdFiEEWPb2jX6FS2mqyJRQLmK0HJOGlEMFAl00zmEACgkQLmK0HJOG
70 lEMDFBAAhQKKqLD1VICygJMEB8t1gBmNLgvziOLfpX4KPWdPtBk3v/QJ7OrfMrVK
71 xlC4ZZyx6yMm1Q7GzmuWykmZQJ9HMaHJ49KAbh5MMjjV/+OoQw9coIdo8nagRUld
72 vX8QHzNZ6Agx77xHuDJZgdHKpQK3TrMDsxzoYYMvlqoLJIDXE1Sp7KYNy12nhdRg
73 R6NXNmW8oMZuxglkmUwayMiPS+N4zNYqv0CXYzlEqCOgq9MJUcAMHt+KpiST+sm6
74 FWkJ9D+biNPyQ9QKf1AE4BdZia4lHfPYU/C/DEL/a5xQuuop/zMQZoGaIA4p2zGQ
75 /maqYxEIM/yRBQpT1jlODKPJrMEgx7SgY2hRU47YZ4fj6350fb6fNBtiiMAfJbjL
76 S3Gh85E9fm3hJaNSPKAaJFYL1Ya2svuWfgHj677C56UcmYis7fhiiy1aJuYdHnSm
77 sD53z/f0J+We4VZjY+pidvA9BGZPFVdR3wd3xGs8/oH6UWaLJAMGkLG6dDb3qDLm
78 1LFZwsX8sdD32i1SiWanYQYSYMyFWr0awi4xdoMtYCL7uKBYtwtPyvq3cj4IrJlb
79 mfeFhT57UbE4qukTDIQ0Y0WM40UYRTakRaDY7ubhXgLgx09Cnp9XTVMsHgT6j9/i
80 1pxsB104XLWjQHTjr1JtiaBQEwFh9r2OKTcpvaLcbNtYpo7CzOs=
81 =FRsO
82 -----END PGP SIGNATURE-----
83
84empty commit`
85
86 sha := &Sha1Hash{0xfe, 0xaf, 0x4b, 0xa6, 0xbc, 0x63, 0x5f, 0xec, 0x44, 0x2f, 0x46, 0xdd, 0xd4, 0x51, 0x24, 0x16, 0xec, 0x43, 0xc2, 0xc2}
87 gitRepo, err := openRepositoryWithDefaultContext(filepath.Join(testReposDir, "repo1_bare"))
88 require.NoError(t, err)
89 assert.NotNil(t, gitRepo)
90 defer gitRepo.Close()
91
92 commitFromReader, err := CommitFromReader(gitRepo, sha, strings.NewReader(commitString))
93 require.NoError(t, err)
94 require.NotNil(t, commitFromReader)
95 assert.EqualValues(t, sha, commitFromReader.ID)
96 assert.Equal(t, `-----BEGIN PGP SIGNATURE-----
97
98iQIzBAABCAAdFiEEWPb2jX6FS2mqyJRQLmK0HJOGlEMFAl00zmEACgkQLmK0HJOG
99lEMDFBAAhQKKqLD1VICygJMEB8t1gBmNLgvziOLfpX4KPWdPtBk3v/QJ7OrfMrVK
100xlC4ZZyx6yMm1Q7GzmuWykmZQJ9HMaHJ49KAbh5MMjjV/+OoQw9coIdo8nagRUld
101vX8QHzNZ6Agx77xHuDJZgdHKpQK3TrMDsxzoYYMvlqoLJIDXE1Sp7KYNy12nhdRg
102R6NXNmW8oMZuxglkmUwayMiPS+N4zNYqv0CXYzlEqCOgq9MJUcAMHt+KpiST+sm6
103FWkJ9D+biNPyQ9QKf1AE4BdZia4lHfPYU/C/DEL/a5xQuuop/zMQZoGaIA4p2zGQ
104/maqYxEIM/yRBQpT1jlODKPJrMEgx7SgY2hRU47YZ4fj6350fb6fNBtiiMAfJbjL
105S3Gh85E9fm3hJaNSPKAaJFYL1Ya2svuWfgHj677C56UcmYis7fhiiy1aJuYdHnSm
106sD53z/f0J+We4VZjY+pidvA9BGZPFVdR3wd3xGs8/oH6UWaLJAMGkLG6dDb3qDLm
1071LFZwsX8sdD32i1SiWanYQYSYMyFWr0awi4xdoMtYCL7uKBYtwtPyvq3cj4IrJlb
108mfeFhT57UbE4qukTDIQ0Y0WM40UYRTakRaDY7ubhXgLgx09Cnp9XTVMsHgT6j9/i
1091pxsB104XLWjQHTjr1JtiaBQEwFh9r2OKTcpvaLcbNtYpo7CzOs=
110=FRsO
111-----END PGP SIGNATURE-----
112`, commitFromReader.Signature.Signature)
113 assert.Equal(t, `tree f1a6cb52b2d16773290cefe49ad0684b50a4f930
114parent 37991dec2c8e592043f47155ce4808d4580f9123
115author silverwind <me@silverwind.io> 1563741793 +0200
116committer silverwind <me@silverwind.io> 1563741793 +0200
117
118empty commit`, commitFromReader.Signature.Payload)
119 assert.Equal(t, "silverwind <me@silverwind.io>", commitFromReader.Author.String())
120
121 commitFromReader2, err := CommitFromReader(gitRepo, sha, strings.NewReader(commitString+"\n\n"))
122 require.NoError(t, err)
123 commitFromReader.CommitMessage += "\n\n"
124 commitFromReader.Signature.Payload += "\n\n"
125 assert.Equal(t, commitFromReader, commitFromReader2)
126}
127
128func TestCommitWithEncodingFromReader(t *testing.T) {
129 commitString := `feaf4ba6bc635fec442f46ddd4512416ec43c2c2 commit 1074
130tree ca3fad42080dd1a6d291b75acdfc46e5b9b307e5
131parent 47b24e7ab977ed31c5a39989d570847d6d0052af
132author KN4CK3R <admin@oldschoolhack.me> 1711702962 +0100
133committer KN4CK3R <admin@oldschoolhack.me> 1711702962 +0100
134encoding ISO-8859-1
135gpgsig -----BEGIN PGP SIGNATURE-----
136` + " " + `
137 iQGzBAABCgAdFiEE9HRrbqvYxPT8PXbefPSEkrowAa8FAmYGg7IACgkQfPSEkrow
138 Aa9olwv+P0HhtCM6CRvlUmPaqswRsDPNR4i66xyXGiSxdI9V5oJL7HLiQIM7KrFR
139 gizKa2COiGtugv8fE+TKqXKaJx6uJUJEjaBd8E9Af9PrAzjWj+A84lU6/PgPS8hq
140 zOfZraLOEWRH4tZcS+u2yFLu3ez2Wqh1xW5LNy7xqEedMXEFD1HwSJ0+pjacNkzr
141 frp6Asyt7xRI6YmgFJZJoRsS3Ktr6rtKeRL2IErSQQyorOqj6gKrglhrhfG/114j
142 FKB1v4or0WZ1DE8iP2SJZ3n+/K1IuWAINh7MVdb7PndfBPEa+IL+ucNk5uzEE8Jd
143 G8smGxXUeFEt2cP1dj2W8EgAxuA9sTnH9dqI5aRqy5ifDjuya7Emm8sdOUvtGdmn
144 SONRzusmu5n3DgV956REL7x62h7JuqmBz/12HZkr0z0zgXkcZ04q08pSJATX5N1F
145 yN+tWxTsWg+zhDk96d5Esdo9JMjcFvPv0eioo30GAERaz1hoD7zCMT4jgUFTQwgz
146 jw4YcO5u
147 =r3UU
148 -----END PGP SIGNATURE-----
149
150ISO-8859-1`
151
152 sha := &Sha1Hash{0xfe, 0xaf, 0x4b, 0xa6, 0xbc, 0x63, 0x5f, 0xec, 0x44, 0x2f, 0x46, 0xdd, 0xd4, 0x51, 0x24, 0x16, 0xec, 0x43, 0xc2, 0xc2}
153 gitRepo, err := openRepositoryWithDefaultContext(filepath.Join(testReposDir, "repo1_bare"))
154 require.NoError(t, err)
155 assert.NotNil(t, gitRepo)
156 defer gitRepo.Close()
157
158 commitFromReader, err := CommitFromReader(gitRepo, sha, strings.NewReader(commitString))
159 require.NoError(t, err)
160 require.NotNil(t, commitFromReader)
161 assert.EqualValues(t, sha, commitFromReader.ID)
162 assert.Equal(t, `-----BEGIN PGP SIGNATURE-----
163
164iQGzBAABCgAdFiEE9HRrbqvYxPT8PXbefPSEkrowAa8FAmYGg7IACgkQfPSEkrow
165Aa9olwv+P0HhtCM6CRvlUmPaqswRsDPNR4i66xyXGiSxdI9V5oJL7HLiQIM7KrFR
166gizKa2COiGtugv8fE+TKqXKaJx6uJUJEjaBd8E9Af9PrAzjWj+A84lU6/PgPS8hq
167zOfZraLOEWRH4tZcS+u2yFLu3ez2Wqh1xW5LNy7xqEedMXEFD1HwSJ0+pjacNkzr
168frp6Asyt7xRI6YmgFJZJoRsS3Ktr6rtKeRL2IErSQQyorOqj6gKrglhrhfG/114j
169FKB1v4or0WZ1DE8iP2SJZ3n+/K1IuWAINh7MVdb7PndfBPEa+IL+ucNk5uzEE8Jd
170G8smGxXUeFEt2cP1dj2W8EgAxuA9sTnH9dqI5aRqy5ifDjuya7Emm8sdOUvtGdmn
171SONRzusmu5n3DgV956REL7x62h7JuqmBz/12HZkr0z0zgXkcZ04q08pSJATX5N1F
172yN+tWxTsWg+zhDk96d5Esdo9JMjcFvPv0eioo30GAERaz1hoD7zCMT4jgUFTQwgz
173jw4YcO5u
174=r3UU
175-----END PGP SIGNATURE-----
176`, commitFromReader.Signature.Signature)
177 assert.Equal(t, `tree ca3fad42080dd1a6d291b75acdfc46e5b9b307e5
178parent 47b24e7ab977ed31c5a39989d570847d6d0052af
179author KN4CK3R <admin@oldschoolhack.me> 1711702962 +0100
180committer KN4CK3R <admin@oldschoolhack.me> 1711702962 +0100
181encoding ISO-8859-1
182
183ISO-8859-1`, commitFromReader.Signature.Payload)
184 assert.Equal(t, "KN4CK3R <admin@oldschoolhack.me>", commitFromReader.Author.String())
185
186 commitFromReader2, err := CommitFromReader(gitRepo, sha, strings.NewReader(commitString+"\n\n"))
187 require.NoError(t, err)
188 commitFromReader.CommitMessage += "\n\n"
189 commitFromReader.Signature.Payload += "\n\n"
190 assert.Equal(t, commitFromReader, commitFromReader2)
191}
192
193func TestHasPreviousCommit(t *testing.T) {
194 bareRepo1Path := filepath.Join(testReposDir, "repo1_bare")
195
196 repo, err := openRepositoryWithDefaultContext(bareRepo1Path)
197 require.NoError(t, err)
198 defer repo.Close()
199
200 commit, err := repo.GetCommit("8006ff9adbf0cb94da7dad9e537e53817f9fa5c0")
201 require.NoError(t, err)
202
203 parentSHA := MustIDFromString("8d92fc957a4d7cfd98bc375f0b7bb189a0d6c9f2")
204 notParentSHA := MustIDFromString("2839944139e0de9737a044f78b0e4b40d989a9e3")
205
206 haz, err := commit.HasPreviousCommit(parentSHA)
207 require.NoError(t, err)
208 assert.True(t, haz)
209
210 hazNot, err := commit.HasPreviousCommit(notParentSHA)
211 require.NoError(t, err)
212 assert.False(t, hazNot)
213
214 selfNot, err := commit.HasPreviousCommit(commit.ID)
215 require.NoError(t, err)
216 assert.False(t, selfNot)
217}
218
219func TestParseCommitFileStatus(t *testing.T) {
220 type testcase struct {
221 output string
222 added []string
223 removed []string
224 modified []string
225 }
226
227 kases := []testcase{
228 {
229 // Merge commit
230 output: "MM\x00options/locale/locale_en-US.ini\x00",
231 modified: []string{
232 "options/locale/locale_en-US.ini",
233 },
234 added: []string{},
235 removed: []string{},
236 },
237 {
238 // Spaces commit
239 output: "D\x00b\x00D\x00b b/b\x00A\x00b b/b b/b b/b\x00A\x00b b/b b/b b/b b/b\x00",
240 removed: []string{
241 "b",
242 "b b/b",
243 },
244 modified: []string{},
245 added: []string{
246 "b b/b b/b b/b",
247 "b b/b b/b b/b b/b",
248 },
249 },
250 {
251 // larger commit
252 output: "M\x00go.mod\x00M\x00go.sum\x00M\x00modules/ssh/ssh.go\x00M\x00vendor/github.com/gliderlabs/ssh/circle.yml\x00M\x00vendor/github.com/gliderlabs/ssh/context.go\x00A\x00vendor/github.com/gliderlabs/ssh/go.mod\x00A\x00vendor/github.com/gliderlabs/ssh/go.sum\x00M\x00vendor/github.com/gliderlabs/ssh/server.go\x00M\x00vendor/github.com/gliderlabs/ssh/session.go\x00M\x00vendor/github.com/gliderlabs/ssh/ssh.go\x00M\x00vendor/golang.org/x/sys/unix/mkerrors.sh\x00M\x00vendor/golang.org/x/sys/unix/syscall_darwin.go\x00M\x00vendor/golang.org/x/sys/unix/zerrors_darwin_amd64.go\x00M\x00vendor/golang.org/x/sys/unix/zerrors_darwin_arm64.go\x00M\x00vendor/golang.org/x/sys/unix/zerrors_freebsd_386.go\x00M\x00vendor/golang.org/x/sys/unix/zerrors_freebsd_amd64.go\x00M\x00vendor/golang.org/x/sys/unix/zerrors_freebsd_arm.go\x00M\x00vendor/golang.org/x/sys/unix/zerrors_freebsd_arm64.go\x00M\x00vendor/golang.org/x/sys/unix/zerrors_linux.go\x00M\x00vendor/golang.org/x/sys/unix/ztypes_darwin_amd64.go\x00M\x00vendor/golang.org/x/sys/unix/ztypes_darwin_arm64.go\x00M\x00vendor/golang.org/x/sys/unix/ztypes_dragonfly_amd64.go\x00M\x00vendor/golang.org/x/sys/unix/ztypes_freebsd_386.go\x00M\x00vendor/golang.org/x/sys/unix/ztypes_freebsd_amd64.go\x00M\x00vendor/golang.org/x/sys/unix/ztypes_freebsd_arm.go\x00M\x00vendor/golang.org/x/sys/unix/ztypes_freebsd_arm64.go\x00M\x00vendor/golang.org/x/sys/unix/ztypes_netbsd_386.go\x00M\x00vendor/golang.org/x/sys/unix/ztypes_netbsd_amd64.go\x00M\x00vendor/golang.org/x/sys/unix/ztypes_netbsd_arm.go\x00M\x00vendor/golang.org/x/sys/unix/ztypes_netbsd_arm64.go\x00M\x00vendor/modules.txt\x00",
253 modified: []string{
254 "go.mod",
255 "go.sum",
256 "modules/ssh/ssh.go",
257 "vendor/github.com/gliderlabs/ssh/circle.yml",
258 "vendor/github.com/gliderlabs/ssh/context.go",
259 "vendor/github.com/gliderlabs/ssh/server.go",
260 "vendor/github.com/gliderlabs/ssh/session.go",
261 "vendor/github.com/gliderlabs/ssh/ssh.go",
262 "vendor/golang.org/x/sys/unix/mkerrors.sh",
263 "vendor/golang.org/x/sys/unix/syscall_darwin.go",
264 "vendor/golang.org/x/sys/unix/zerrors_darwin_amd64.go",
265 "vendor/golang.org/x/sys/unix/zerrors_darwin_arm64.go",
266 "vendor/golang.org/x/sys/unix/zerrors_freebsd_386.go",
267 "vendor/golang.org/x/sys/unix/zerrors_freebsd_amd64.go",
268 "vendor/golang.org/x/sys/unix/zerrors_freebsd_arm.go",
269 "vendor/golang.org/x/sys/unix/zerrors_freebsd_arm64.go",
270 "vendor/golang.org/x/sys/unix/zerrors_linux.go",
271 "vendor/golang.org/x/sys/unix/ztypes_darwin_amd64.go",
272 "vendor/golang.org/x/sys/unix/ztypes_darwin_arm64.go",
273 "vendor/golang.org/x/sys/unix/ztypes_dragonfly_amd64.go",
274 "vendor/golang.org/x/sys/unix/ztypes_freebsd_386.go",
275 "vendor/golang.org/x/sys/unix/ztypes_freebsd_amd64.go",
276 "vendor/golang.org/x/sys/unix/ztypes_freebsd_arm.go",
277 "vendor/golang.org/x/sys/unix/ztypes_freebsd_arm64.go",
278 "vendor/golang.org/x/sys/unix/ztypes_netbsd_386.go",
279 "vendor/golang.org/x/sys/unix/ztypes_netbsd_amd64.go",
280 "vendor/golang.org/x/sys/unix/ztypes_netbsd_arm.go",
281 "vendor/golang.org/x/sys/unix/ztypes_netbsd_arm64.go",
282 "vendor/modules.txt",
283 },
284 added: []string{
285 "vendor/github.com/gliderlabs/ssh/go.mod",
286 "vendor/github.com/gliderlabs/ssh/go.sum",
287 },
288 removed: []string{},
289 },
290 {
291 // git 1.7.2 adds an unnecessary \x00 on merge commit
292 output: "\x00MM\x00options/locale/locale_en-US.ini\x00",
293 modified: []string{
294 "options/locale/locale_en-US.ini",
295 },
296 added: []string{},
297 removed: []string{},
298 },
299 {
300 // git 1.7.2 adds an unnecessary \n on normal commit
301 output: "\nD\x00b\x00D\x00b b/b\x00A\x00b b/b b/b b/b\x00A\x00b b/b b/b b/b b/b\x00",
302 removed: []string{
303 "b",
304 "b b/b",
305 },
306 modified: []string{},
307 added: []string{
308 "b b/b b/b b/b",
309 "b b/b b/b b/b b/b",
310 },
311 },
312 }
313
314 for _, kase := range kases {
315 fileStatus := NewCommitFileStatus()
316 parseCommitFileStatus(fileStatus, strings.NewReader(kase.output))
317
318 assert.Equal(t, kase.added, fileStatus.Added)
319 assert.Equal(t, kase.removed, fileStatus.Removed)
320 assert.Equal(t, kase.modified, fileStatus.Modified)
321 }
322}
323
324func TestGetCommitFileStatusMerges(t *testing.T) {
325 bareRepo1Path := filepath.Join(testReposDir, "repo6_merge")
326
327 commitFileStatus, err := GetCommitFileStatus(DefaultContext, bareRepo1Path, "022f4ce6214973e018f02bf363bf8a2e3691f699")
328 require.NoError(t, err)
329
330 expected := CommitFileStatus{
331 []string{
332 "add_file.txt",
333 },
334 []string{
335 "to_remove.txt",
336 },
337 []string{
338 "to_modify.txt",
339 },
340 }
341
342 assert.Equal(t, expected.Added, commitFileStatus.Added)
343 assert.Equal(t, expected.Removed, commitFileStatus.Removed)
344 assert.Equal(t, expected.Modified, commitFileStatus.Modified)
345}
346
347func TestParseCommitRenames(t *testing.T) {
348 testcases := []struct {
349 output string
350 renames [][2]string
351 }{
352 {
353 output: "R090\x00renamed.txt\x00history.txt\x00",
354 renames: [][2]string{{"renamed.txt", "history.txt"}},
355 },
356 {
357 output: "R090\x00renamed.txt\x00history.txt\x00R000\x00corruptedstdouthere",
358 renames: [][2]string{{"renamed.txt", "history.txt"}},
359 },
360 {
361 output: "R100\x00renamed.txt\x00history.txt\x00R001\x00readme.md\x00README.md\x00",
362 renames: [][2]string{{"renamed.txt", "history.txt"}, {"readme.md", "README.md"}},
363 },
364 }
365
366 for _, testcase := range testcases {
367 renames := [][2]string{}
368 parseCommitRenames(&renames, strings.NewReader(testcase.output))
369
370 assert.Equal(t, testcase.renames, renames)
371 }
372}
373
374func TestGetAllBranches(t *testing.T) {
375 bareRepo1Path := filepath.Join(testReposDir, "repo1_bare")
376
377 bareRepo1, err := openRepositoryWithDefaultContext(bareRepo1Path)
378 require.NoError(t, err)
379
380 commit, err := bareRepo1.GetCommit("95bb4d39648ee7e325106df01a621c530863a653")
381 require.NoError(t, err)
382
383 branches, err := commit.GetAllBranches()
384 require.NoError(t, err)
385
386 slices.Sort(branches)
387
388 assert.Equal(t, []string{"branch1", "branch2", "master"}, branches)
389}
390
391func Test_parseSubmoduleContent(t *testing.T) {
392 submoduleFiles := []struct {
393 fileContent string
394 expectedPath string
395 expectedURL string
396 }{
397 {
398 fileContent: `[submodule "jakarta-servlet"]
399url = ../../ALP-pool/jakarta-servlet
400path = jakarta-servlet`,
401 expectedPath: "jakarta-servlet",
402 expectedURL: "../../ALP-pool/jakarta-servlet",
403 },
404 {
405 fileContent: `[submodule "jakarta-servlet"]
406path = jakarta-servlet
407url = ../../ALP-pool/jakarta-servlet`,
408 expectedPath: "jakarta-servlet",
409 expectedURL: "../../ALP-pool/jakarta-servlet",
410 },
411 }
412 for _, kase := range submoduleFiles {
413 submodule, err := parseSubmoduleContent([]byte(kase.fileContent))
414 require.NoError(t, err)
415 v, ok := submodule.Get(kase.expectedPath)
416 assert.True(t, ok)
417 assert.Equal(t, kase.expectedURL, v)
418 }
419}