1package git
2
3import (
4 "bytes"
5 "context"
6 "errors"
7 "fmt"
8 "io"
9 "os"
10 "path/filepath"
11 "regexp"
12 "runtime"
13 "strings"
14 "testing"
15 "time"
16
17 fixtures "github.com/go-git/go-git-fixtures/v4"
18 "github.com/go-git/go-git/v5/config"
19 "github.com/go-git/go-git/v5/plumbing"
20 "github.com/go-git/go-git/v5/plumbing/cache"
21 "github.com/go-git/go-git/v5/plumbing/filemode"
22 "github.com/go-git/go-git/v5/plumbing/format/gitignore"
23 "github.com/go-git/go-git/v5/plumbing/format/index"
24 "github.com/go-git/go-git/v5/plumbing/object"
25 "github.com/go-git/go-git/v5/storage/filesystem"
26 "github.com/go-git/go-git/v5/storage/memory"
27 "github.com/stretchr/testify/assert"
28 "github.com/stretchr/testify/suite"
29
30 "github.com/go-git/go-billy/v5"
31 "github.com/go-git/go-billy/v5/memfs"
32 "github.com/go-git/go-billy/v5/osfs"
33 "github.com/go-git/go-billy/v5/util"
34 "golang.org/x/text/unicode/norm"
35)
36
37func defaultTestCommitOptions() *CommitOptions {
38 return &CommitOptions{
39 Author: &object.Signature{Name: "testuser", Email: "testemail"},
40 }
41}
42
43type WorktreeSuite struct {
44 suite.Suite
45 BaseSuite
46}
47
48func TestWorktreeSuite(t *testing.T) {
49 suite.Run(t, new(WorktreeSuite))
50}
51
52func (s *WorktreeSuite) SetupTest() {
53 f := fixtures.Basic().One()
54 s.Repository = NewRepositoryWithEmptyWorktree(f)
55}
56
57func (s *WorktreeSuite) TestPullCheckout() {
58 fs := memfs.New()
59 r, _ := Init(memory.NewStorage(), fs)
60 r.CreateRemote(&config.RemoteConfig{
61 Name: DefaultRemoteName,
62 URLs: []string{s.GetBasicLocalRepositoryURL()},
63 })
64
65 w, err := r.Worktree()
66 s.NoError(err)
67
68 err = w.Pull(&PullOptions{})
69 s.NoError(err)
70
71 fi, err := fs.ReadDir("")
72 s.NoError(err)
73 s.Len(fi, 8)
74}
75
76func (s *WorktreeSuite) TestPullFastForward() {
77 url, err := os.MkdirTemp("", "")
78 s.NoError(err)
79
80 path := fixtures.Basic().ByTag("worktree").One().Worktree().Root()
81
82 server, err := PlainClone(url, false, &CloneOptions{
83 URL: path,
84 })
85 s.NoError(err)
86
87 dir, err := os.MkdirTemp("", "")
88 s.NoError(err)
89
90 r, err := PlainClone(dir, false, &CloneOptions{
91 URL: url,
92 })
93 s.NoError(err)
94
95 w, err := server.Worktree()
96 s.NoError(err)
97 err = os.WriteFile(filepath.Join(url, "foo"), []byte("foo"), 0o755)
98 s.NoError(err)
99 w.Add("foo")
100 hash, err := w.Commit("foo", &CommitOptions{Author: defaultSignature()})
101 s.NoError(err)
102
103 w, err = r.Worktree()
104 s.NoError(err)
105
106 err = w.Pull(&PullOptions{})
107 s.NoError(err)
108
109 head, err := r.Head()
110 s.NoError(err)
111 s.Equal(hash, head.Hash())
112}
113
114func (s *WorktreeSuite) TestPullNonFastForward() {
115 url, err := os.MkdirTemp("", "")
116 s.NoError(err)
117
118 path := fixtures.Basic().ByTag("worktree").One().Worktree().Root()
119
120 server, err := PlainClone(url, false, &CloneOptions{
121 URL: path,
122 })
123 s.NoError(err)
124
125 dir, err := os.MkdirTemp("", "")
126 s.NoError(err)
127
128 r, err := PlainClone(dir, false, &CloneOptions{
129 URL: url,
130 })
131 s.NoError(err)
132
133 w, err := server.Worktree()
134 s.NoError(err)
135 err = os.WriteFile(filepath.Join(url, "foo"), []byte("foo"), 0o755)
136 s.NoError(err)
137 w.Add("foo")
138 _, err = w.Commit("foo", &CommitOptions{Author: defaultSignature()})
139 s.NoError(err)
140
141 w, err = r.Worktree()
142 s.NoError(err)
143 err = os.WriteFile(filepath.Join(dir, "bar"), []byte("bar"), 0o755)
144 s.NoError(err)
145 w.Add("bar")
146 _, err = w.Commit("bar", &CommitOptions{Author: defaultSignature()})
147 s.NoError(err)
148
149 err = w.Pull(&PullOptions{})
150 s.ErrorIs(err, ErrNonFastForwardUpdate)
151}
152
153func (s *WorktreeSuite) TestPullUpdateReferencesIfNeeded() {
154 r, _ := Init(memory.NewStorage(), memfs.New())
155 r.CreateRemote(&config.RemoteConfig{
156 Name: DefaultRemoteName,
157 URLs: []string{s.GetBasicLocalRepositoryURL()},
158 })
159
160 err := r.Fetch(&FetchOptions{})
161 s.NoError(err)
162
163 _, err = r.Reference("refs/heads/master", false)
164 s.NotNil(err)
165
166 w, err := r.Worktree()
167 s.NoError(err)
168
169 err = w.Pull(&PullOptions{})
170 s.NoError(err)
171
172 head, err := r.Reference(plumbing.HEAD, true)
173 s.NoError(err)
174 s.Equal("6ecf0ef2c2dffb796033e5a02219af86ec6584e5", head.Hash().String())
175
176 branch, err := r.Reference("refs/heads/master", false)
177 s.NoError(err)
178 s.Equal("6ecf0ef2c2dffb796033e5a02219af86ec6584e5", branch.Hash().String())
179
180 err = w.Pull(&PullOptions{})
181 s.ErrorIs(err, NoErrAlreadyUpToDate)
182}
183
184func (s *WorktreeSuite) TestPullInSingleBranch() {
185 r, _ := Init(memory.NewStorage(), memfs.New())
186 err := r.clone(context.Background(), &CloneOptions{
187 URL: s.GetBasicLocalRepositoryURL(),
188 SingleBranch: true,
189 })
190
191 s.NoError(err)
192
193 w, err := r.Worktree()
194 s.NoError(err)
195
196 err = w.Pull(&PullOptions{})
197 s.ErrorIs(err, NoErrAlreadyUpToDate)
198
199 branch, err := r.Reference("refs/heads/master", false)
200 s.NoError(err)
201 s.Equal("6ecf0ef2c2dffb796033e5a02219af86ec6584e5", branch.Hash().String())
202
203 _, err = r.Reference("refs/remotes/foo/branch", false)
204 s.NotNil(err)
205
206 storage := r.Storer.(*memory.Storage)
207 s.Len(storage.Objects, 28)
208}
209
210func (s *WorktreeSuite) TestPullProgress() {
211 r, _ := Init(memory.NewStorage(), memfs.New())
212
213 r.CreateRemote(&config.RemoteConfig{
214 Name: DefaultRemoteName,
215 URLs: []string{s.GetBasicLocalRepositoryURL()},
216 })
217
218 w, err := r.Worktree()
219 s.NoError(err)
220
221 buf := bytes.NewBuffer(nil)
222 err = w.Pull(&PullOptions{
223 Progress: buf,
224 })
225
226 s.NoError(err)
227 s.NotEqual(0, buf.Len())
228}
229
230func (s *WorktreeSuite) TestPullProgressWithRecursion() {
231 if testing.Short() {
232 s.T().Skip("skipping test in short mode.")
233 }
234
235 path := fixtures.ByTag("submodule").One().Worktree().Root()
236
237 dir, err := os.MkdirTemp("", "")
238 s.NoError(err)
239
240 r, _ := PlainInit(dir, false)
241 r.CreateRemote(&config.RemoteConfig{
242 Name: DefaultRemoteName,
243 URLs: []string{path},
244 })
245
246 w, err := r.Worktree()
247 s.NoError(err)
248
249 err = w.Pull(&PullOptions{
250 RecurseSubmodules: DefaultSubmoduleRecursionDepth,
251 })
252 s.NoError(err)
253
254 cfg, err := r.Config()
255 s.NoError(err)
256 s.Len(cfg.Submodules, 2)
257}
258
259func (s *RepositorySuite) TestPullAdd() {
260 path := fixtures.Basic().ByTag("worktree").One().Worktree().Root()
261
262 r, err := Clone(memory.NewStorage(), memfs.New(), &CloneOptions{
263 URL: filepath.Join(path, ".git"),
264 })
265
266 s.NoError(err)
267
268 storage := r.Storer.(*memory.Storage)
269 s.Len(storage.Objects, 28)
270
271 branch, err := r.Reference("refs/heads/master", false)
272 s.NoError(err)
273 s.Equal("6ecf0ef2c2dffb796033e5a02219af86ec6584e5", branch.Hash().String())
274
275 ExecuteOnPath(s.T(), path,
276 "touch foo",
277 "git add foo",
278 "git commit --no-gpg-sign -m foo foo",
279 )
280
281 w, err := r.Worktree()
282 s.NoError(err)
283
284 err = w.Pull(&PullOptions{RemoteName: "origin"})
285 s.NoError(err)
286
287 // the commit command has introduced a new commit, tree and blob
288 s.Len(storage.Objects, 31)
289
290 branch, err = r.Reference("refs/heads/master", false)
291 s.NoError(err)
292 s.NotEqual("6ecf0ef2c2dffb796033e5a02219af86ec6584e5", branch.Hash().String())
293}
294
295func (s *WorktreeSuite) TestPullAlreadyUptodate() {
296 path := fixtures.Basic().ByTag("worktree").One().Worktree().Root()
297
298 fs := memfs.New()
299 r, err := Clone(memory.NewStorage(), fs, &CloneOptions{
300 URL: filepath.Join(path, ".git"),
301 })
302
303 s.NoError(err)
304
305 w, err := r.Worktree()
306 s.NoError(err)
307 err = util.WriteFile(fs, "bar", []byte("bar"), 0o755)
308 s.NoError(err)
309 w.Add("bar")
310 _, err = w.Commit("bar", &CommitOptions{Author: defaultSignature()})
311 s.NoError(err)
312
313 err = w.Pull(&PullOptions{})
314 s.ErrorIs(err, NoErrAlreadyUpToDate)
315}
316
317func (s *WorktreeSuite) TestPullDepth() {
318 r, err := Clone(memory.NewStorage(), memfs.New(), &CloneOptions{
319 URL: fixtures.Basic().One().URL,
320 Depth: 1,
321 })
322
323 s.NoError(err)
324
325 w, err := r.Worktree()
326 s.NoError(err)
327 err = w.Pull(&PullOptions{})
328 s.NoError(err)
329}
330
331func (s *WorktreeSuite) TestPullAfterShallowClone() {
332 tempDir, err := os.MkdirTemp("", "")
333 s.NoError(err)
334 remoteURL := filepath.Join(tempDir, "remote")
335 repoDir := filepath.Join(tempDir, "repo")
336
337 remote, err := PlainInit(remoteURL, false)
338 s.NoError(err)
339 s.NotNil(remote)
340
341 _ = CommitNewFile(s.T(), remote, "File1")
342 _ = CommitNewFile(s.T(), remote, "File2")
343
344 repo, err := PlainClone(repoDir, false, &CloneOptions{
345 URL: remoteURL,
346 Depth: 1,
347 Tags: plumbing.NoTags,
348 SingleBranch: true,
349 ReferenceName: "master",
350 })
351 s.NoError(err)
352
353 _ = CommitNewFile(s.T(), remote, "File3")
354 _ = CommitNewFile(s.T(), remote, "File4")
355
356 w, err := repo.Worktree()
357 s.NoError(err)
358
359 err = w.Pull(&PullOptions{
360 RemoteName: DefaultRemoteName,
361 SingleBranch: true,
362 ReferenceName: plumbing.NewBranchReferenceName("master"),
363 })
364 s.NoError(err)
365}
366
367func (s *WorktreeSuite) TestCheckout() {
368 fs := memfs.New()
369 w := &Worktree{
370 r: s.Repository,
371 Filesystem: fs,
372 }
373
374 err := w.Checkout(&CheckoutOptions{
375 Force: true,
376 })
377 s.NoError(err)
378
379 entries, err := fs.ReadDir("/")
380 s.NoError(err)
381
382 s.Len(entries, 8)
383 ch, err := fs.Open("CHANGELOG")
384 s.NoError(err)
385
386 content, err := io.ReadAll(ch)
387 s.NoError(err)
388 s.Equal("Initial changelog\n", string(content))
389
390 idx, err := s.Repository.Storer.Index()
391 s.NoError(err)
392 s.Len(idx.Entries, 9)
393}
394
395func (s *WorktreeSuite) TestCheckoutForce() {
396 w := &Worktree{
397 r: s.Repository,
398 Filesystem: memfs.New(),
399 }
400
401 err := w.Checkout(&CheckoutOptions{})
402 s.NoError(err)
403
404 w.Filesystem = memfs.New()
405
406 err = w.Checkout(&CheckoutOptions{
407 Force: true,
408 })
409 s.NoError(err)
410
411 entries, err := w.Filesystem.ReadDir("/")
412 s.NoError(err)
413 s.Len(entries, 8)
414}
415
416func (s *WorktreeSuite) TestCheckoutKeep() {
417 w := &Worktree{
418 r: s.Repository,
419 Filesystem: memfs.New(),
420 }
421
422 err := w.Checkout(&CheckoutOptions{
423 Force: true,
424 })
425 s.NoError(err)
426
427 // Create a new branch and create a new file.
428 err = w.Checkout(&CheckoutOptions{
429 Branch: plumbing.NewBranchReferenceName("new-branch"),
430 Create: true,
431 })
432 s.NoError(err)
433
434 w.Filesystem = memfs.New()
435 f, err := w.Filesystem.Create("new-file.txt")
436 s.NoError(err)
437 _, err = f.Write([]byte("DUMMY"))
438 s.NoError(err)
439 s.Nil(f.Close())
440
441 // Add the file to staging.
442 _, err = w.Add("new-file.txt")
443 s.NoError(err)
444
445 // Switch branch to master, and verify that the new file was kept in staging.
446 err = w.Checkout(&CheckoutOptions{
447 Keep: true,
448 })
449 s.NoError(err)
450
451 fi, err := w.Filesystem.Stat("new-file.txt")
452 s.NoError(err)
453 s.Equal(int64(5), fi.Size())
454}
455
456func (s *WorktreeSuite) TestCheckoutSymlink() {
457 if runtime.GOOS == "windows" {
458 s.T().Skip("git doesn't support symlinks by default in windows")
459 }
460
461 dir, err := os.MkdirTemp("", "")
462 s.NoError(err)
463
464 r, err := PlainInit(dir, false)
465 s.NoError(err)
466
467 w, err := r.Worktree()
468 s.NoError(err)
469
470 w.Filesystem.Symlink("not-exists", "bar")
471 w.Add("bar")
472 w.Commit("foo", &CommitOptions{Author: defaultSignature()})
473
474 r.Storer.SetIndex(&index.Index{Version: 2})
475 w.Filesystem = osfs.New(filepath.Join(dir, "worktree-empty"))
476
477 err = w.Checkout(&CheckoutOptions{})
478 s.NoError(err)
479
480 status, err := w.Status()
481 s.NoError(err)
482 s.True(status.IsClean())
483
484 target, err := w.Filesystem.Readlink("bar")
485 s.Equal("not-exists", target)
486 s.NoError(err)
487}
488
489func (s *WorktreeSuite) TestCheckoutSparse() {
490 fs := memfs.New()
491 r, err := Clone(memory.NewStorage(), fs, &CloneOptions{
492 URL: s.GetBasicLocalRepositoryURL(),
493 NoCheckout: true,
494 })
495 s.NoError(err)
496
497 w, err := r.Worktree()
498 s.NoError(err)
499
500 sparseCheckoutDirectories := []string{"go", "json", "php"}
501 s.NoError(w.Checkout(&CheckoutOptions{
502 SparseCheckoutDirectories: sparseCheckoutDirectories,
503 }))
504
505 fis, err := fs.ReadDir("/")
506 s.NoError(err)
507
508 for _, fi := range fis {
509 s.True(fi.IsDir())
510 var oneOfSparseCheckoutDirs bool
511
512 for _, sparseCheckoutDirectory := range sparseCheckoutDirectories {
513 if strings.HasPrefix(fi.Name(), sparseCheckoutDirectory) {
514 oneOfSparseCheckoutDirs = true
515 }
516 }
517 s.True(oneOfSparseCheckoutDirs)
518 }
519}
520
521func (s *WorktreeSuite) TestFilenameNormalization() {
522 if runtime.GOOS == "windows" {
523 s.T().Skip("windows paths may contain non utf-8 sequences")
524 }
525
526 url, err := os.MkdirTemp("", "")
527 s.NoError(err)
528
529 path := fixtures.Basic().ByTag("worktree").One().Worktree().Root()
530
531 server, err := PlainClone(url, false, &CloneOptions{
532 URL: path,
533 })
534 s.NoError(err)
535
536 filename := "페"
537
538 w, err := server.Worktree()
539 s.NoError(err)
540
541 writeFile := func(path string) {
542 err := util.WriteFile(w.Filesystem, path, []byte("foo"), 0o755)
543 s.NoError(err)
544 }
545
546 writeFile(filename)
547 origHash, err := w.Add(filename)
548 s.NoError(err)
549 _, err = w.Commit("foo", &CommitOptions{Author: defaultSignature()})
550 s.NoError(err)
551
552 r, err := Clone(memory.NewStorage(), memfs.New(), &CloneOptions{
553 URL: url,
554 })
555 s.NoError(err)
556
557 w, err = r.Worktree()
558 s.NoError(err)
559
560 status, err := w.Status()
561 s.NoError(err)
562 s.True(status.IsClean())
563
564 err = w.Filesystem.Remove(filename)
565 s.NoError(err)
566
567 modFilename := norm.NFKD.String(filename)
568 writeFile(modFilename)
569
570 _, err = w.Add(filename)
571 s.NoError(err)
572 modHash, err := w.Add(modFilename)
573 s.NoError(err)
574 // At this point we've got two files with the same content.
575 // Hence their hashes must be the same.
576 s.True(origHash == modHash)
577
578 status, err = w.Status()
579 s.NoError(err)
580 // However, their names are different and the work tree is still dirty.
581 s.False(status.IsClean())
582
583 // Revert back the deletion of the first file.
584 writeFile(filename)
585 _, err = w.Add(filename)
586 s.NoError(err)
587
588 status, err = w.Status()
589 s.NoError(err)
590 // Still dirty - the second file is added.
591 s.False(status.IsClean())
592
593 _, err = w.Remove(modFilename)
594 s.NoError(err)
595
596 status, err = w.Status()
597 s.NoError(err)
598 s.True(status.IsClean())
599}
600
601func (s *WorktreeSuite) TestCheckoutSubmodule() {
602 url := "https://github.com/git-fixtures/submodule.git"
603 r := NewRepositoryWithEmptyWorktree(fixtures.ByURL(url).One())
604
605 w, err := r.Worktree()
606 s.NoError(err)
607
608 err = w.Checkout(&CheckoutOptions{})
609 s.NoError(err)
610
611 status, err := w.Status()
612 s.NoError(err)
613 s.True(status.IsClean())
614}
615
616func (s *WorktreeSuite) TestCheckoutSubmoduleInitialized() {
617 url := "https://github.com/git-fixtures/submodule.git"
618 r := s.NewRepository(fixtures.ByURL(url).One())
619
620 w, err := r.Worktree()
621 s.NoError(err)
622
623 sub, err := w.Submodules()
624 s.NoError(err)
625
626 err = sub.Update(&SubmoduleUpdateOptions{Init: true})
627 s.NoError(err)
628
629 status, err := w.Status()
630 s.NoError(err)
631 s.True(status.IsClean())
632}
633
634func (s *WorktreeSuite) TestCheckoutRelativePathSubmoduleInitialized() {
635 url := "https://github.com/git-fixtures/submodule.git"
636 r := s.NewRepository(fixtures.ByURL(url).One())
637
638 // modify the .gitmodules from original one
639 file, err := r.wt.OpenFile(".gitmodules", os.O_WRONLY|os.O_TRUNC, 0o666)
640 s.NoError(err)
641
642 n, err := io.WriteString(file, `[submodule "basic"]
643 path = basic
644 url = ../basic.git
645[submodule "itself"]
646 path = itself
647 url = ../submodule.git`)
648 s.NoError(err)
649 s.NotEqual(0, n)
650
651 w, err := r.Worktree()
652 s.NoError(err)
653
654 w.Add(".gitmodules")
655 w.Commit("test", &CommitOptions{})
656
657 // test submodule path
658 modules, err := w.readGitmodulesFile()
659 s.NoError(err)
660
661 s.Equal("../basic.git", modules.Submodules["basic"].URL)
662 s.Equal("../submodule.git", modules.Submodules["itself"].URL)
663
664 basicSubmodule, err := w.Submodule("basic")
665 s.NoError(err)
666 basicRepo, err := basicSubmodule.Repository()
667 s.NoError(err)
668 basicRemotes, err := basicRepo.Remotes()
669 s.NoError(err)
670 s.Equal("https://github.com/git-fixtures/basic.git", basicRemotes[0].Config().URLs[0])
671
672 itselfSubmodule, err := w.Submodule("itself")
673 s.NoError(err)
674 itselfRepo, err := itselfSubmodule.Repository()
675 s.NoError(err)
676 itselfRemotes, err := itselfRepo.Remotes()
677 s.NoError(err)
678 s.Equal("https://github.com/git-fixtures/submodule.git", itselfRemotes[0].Config().URLs[0])
679
680 sub, err := w.Submodules()
681 s.NoError(err)
682
683 err = sub.Update(&SubmoduleUpdateOptions{Init: true, RecurseSubmodules: DefaultSubmoduleRecursionDepth})
684 s.NoError(err)
685
686 status, err := w.Status()
687 s.NoError(err)
688 s.True(status.IsClean())
689}
690
691func (s *WorktreeSuite) TestCheckoutIndexMem() {
692 fs := memfs.New()
693 w := &Worktree{
694 r: s.Repository,
695 Filesystem: fs,
696 }
697
698 err := w.Checkout(&CheckoutOptions{})
699 s.NoError(err)
700
701 idx, err := s.Repository.Storer.Index()
702 s.NoError(err)
703 s.Len(idx.Entries, 9)
704 s.Equal("32858aad3c383ed1ff0a0f9bdf231d54a00c9e88", idx.Entries[0].Hash.String())
705 s.Equal(".gitignore", idx.Entries[0].Name)
706 s.Equal(filemode.Regular, idx.Entries[0].Mode)
707 s.False(idx.Entries[0].ModifiedAt.IsZero())
708 s.Equal(uint32(189), idx.Entries[0].Size)
709
710 // ctime, dev, inode, uid and gid are not supported on memfs fs
711 s.True(idx.Entries[0].CreatedAt.IsZero())
712 s.Equal(uint32(0), idx.Entries[0].Dev)
713 s.Equal(uint32(0), idx.Entries[0].Inode)
714 s.Equal(uint32(0), idx.Entries[0].UID)
715 s.Equal(uint32(0), idx.Entries[0].GID)
716}
717
718func (s *WorktreeSuite) TestCheckoutIndexOS() {
719 fs := s.TemporalFilesystem()
720
721 w := &Worktree{
722 r: s.Repository,
723 Filesystem: fs,
724 }
725
726 err := w.Checkout(&CheckoutOptions{})
727 s.NoError(err)
728
729 idx, err := s.Repository.Storer.Index()
730 s.NoError(err)
731 s.Len(idx.Entries, 9)
732 s.Equal("32858aad3c383ed1ff0a0f9bdf231d54a00c9e88", idx.Entries[0].Hash.String())
733 s.Equal(".gitignore", idx.Entries[0].Name)
734 s.Equal(filemode.Regular, idx.Entries[0].Mode)
735 s.False(idx.Entries[0].ModifiedAt.IsZero())
736 s.Equal(uint32(189), idx.Entries[0].Size)
737
738 s.False(idx.Entries[0].CreatedAt.IsZero())
739 if runtime.GOOS != "windows" {
740 s.NotEqual(uint32(0), idx.Entries[0].Dev)
741 s.NotEqual(uint32(0), idx.Entries[0].Inode)
742 s.NotEqual(uint32(0), idx.Entries[0].UID)
743 s.NotEqual(uint32(0), idx.Entries[0].GID)
744 }
745}
746
747func (s *WorktreeSuite) TestCheckoutBranch() {
748 w := &Worktree{
749 r: s.Repository,
750 Filesystem: memfs.New(),
751 }
752
753 err := w.Checkout(&CheckoutOptions{
754 Branch: "refs/heads/branch",
755 })
756 s.NoError(err)
757
758 head, err := w.r.Head()
759 s.NoError(err)
760 s.Equal("refs/heads/branch", head.Name().String())
761
762 status, err := w.Status()
763 s.NoError(err)
764 s.True(status.IsClean())
765}
766
767func (s *WorktreeSuite) TestCheckoutCreateWithHash() {
768 w := &Worktree{
769 r: s.Repository,
770 Filesystem: memfs.New(),
771 }
772
773 err := w.Checkout(&CheckoutOptions{
774 Create: true,
775 Branch: "refs/heads/foo",
776 Hash: plumbing.NewHash("35e85108805c84807bc66a02d91535e1e24b38b9"),
777 })
778 s.NoError(err)
779
780 head, err := w.r.Head()
781 s.NoError(err)
782 s.Equal("refs/heads/foo", head.Name().String())
783 s.Equal(plumbing.NewHash("35e85108805c84807bc66a02d91535e1e24b38b9"), head.Hash())
784
785 status, err := w.Status()
786 s.NoError(err)
787 s.True(status.IsClean())
788}
789
790func (s *WorktreeSuite) TestCheckoutCreate() {
791 w := &Worktree{
792 r: s.Repository,
793 Filesystem: memfs.New(),
794 }
795
796 err := w.Checkout(&CheckoutOptions{
797 Create: true,
798 Branch: "refs/heads/foo",
799 })
800 s.NoError(err)
801
802 head, err := w.r.Head()
803 s.NoError(err)
804 s.Equal("refs/heads/foo", head.Name().String())
805 s.Equal(plumbing.NewHash("6ecf0ef2c2dffb796033e5a02219af86ec6584e5"), head.Hash())
806
807 status, err := w.Status()
808 s.NoError(err)
809 s.True(status.IsClean())
810}
811
812func (s *WorktreeSuite) TestCheckoutBranchAndHash() {
813 w := &Worktree{
814 r: s.Repository,
815 Filesystem: memfs.New(),
816 }
817
818 err := w.Checkout(&CheckoutOptions{
819 Branch: "refs/heads/foo",
820 Hash: plumbing.NewHash("35e85108805c84807bc66a02d91535e1e24b38b9"),
821 })
822
823 s.ErrorIs(err, ErrBranchHashExclusive)
824}
825
826func (s *WorktreeSuite) TestCheckoutCreateMissingBranch() {
827 w := &Worktree{
828 r: s.Repository,
829 Filesystem: memfs.New(),
830 }
831
832 err := w.Checkout(&CheckoutOptions{
833 Create: true,
834 })
835
836 s.ErrorIs(err, ErrCreateRequiresBranch)
837}
838
839func (s *WorktreeSuite) TestCheckoutCreateInvalidBranch() {
840 w := &Worktree{
841 r: s.Repository,
842 Filesystem: memfs.New(),
843 }
844
845 for _, name := range []plumbing.ReferenceName{
846 "foo",
847 "-",
848 "-foo",
849 "refs/heads//",
850 "refs/heads/..",
851 "refs/heads/a..b",
852 "refs/heads/.",
853 } {
854 err := w.Checkout(&CheckoutOptions{
855 Create: true,
856 Branch: name,
857 })
858
859 s.ErrorIs(err, plumbing.ErrInvalidReferenceName)
860 }
861}
862
863func (s *WorktreeSuite) TestCheckoutTag() {
864 f := fixtures.ByTag("tags").One()
865 r := NewRepositoryWithEmptyWorktree(f)
866 w, err := r.Worktree()
867 s.NoError(err)
868
869 err = w.Checkout(&CheckoutOptions{})
870 s.NoError(err)
871 head, err := w.r.Head()
872 s.NoError(err)
873 s.Equal("refs/heads/master", head.Name().String())
874
875 status, err := w.Status()
876 s.NoError(err)
877 s.True(status.IsClean())
878
879 err = w.Checkout(&CheckoutOptions{Branch: "refs/tags/lightweight-tag"})
880 s.NoError(err)
881 head, err = w.r.Head()
882 s.NoError(err)
883 s.Equal("HEAD", head.Name().String())
884 s.Equal("f7b877701fbf855b44c0a9e86f3fdce2c298b07f", head.Hash().String())
885
886 err = w.Checkout(&CheckoutOptions{Branch: "refs/tags/commit-tag"})
887 s.NoError(err)
888 head, err = w.r.Head()
889 s.NoError(err)
890 s.Equal("HEAD", head.Name().String())
891 s.Equal("f7b877701fbf855b44c0a9e86f3fdce2c298b07f", head.Hash().String())
892
893 err = w.Checkout(&CheckoutOptions{Branch: "refs/tags/tree-tag"})
894 s.NotNil(err)
895 head, err = w.r.Head()
896 s.NoError(err)
897 s.Equal("HEAD", head.Name().String())
898}
899
900func (s *WorktreeSuite) TestCheckoutTagHash() {
901 f := fixtures.ByTag("tags").One()
902 r := NewRepositoryWithEmptyWorktree(f)
903 w, err := r.Worktree()
904 s.NoError(err)
905
906 for _, hash := range []string{
907 "b742a2a9fa0afcfa9a6fad080980fbc26b007c69", // annotated tag
908 "ad7897c0fb8e7d9a9ba41fa66072cf06095a6cfc", // commit tag
909 "f7b877701fbf855b44c0a9e86f3fdce2c298b07f", // lightweight tag
910 } {
911 err = w.Checkout(&CheckoutOptions{
912 Hash: plumbing.NewHash(hash),
913 })
914 s.NoError(err)
915 head, err := w.r.Head()
916 s.NoError(err)
917 s.Equal("HEAD", head.Name().String())
918
919 status, err := w.Status()
920 s.NoError(err)
921 s.True(status.IsClean())
922 }
923
924 for _, hash := range []string{
925 "fe6cb94756faa81e5ed9240f9191b833db5f40ae", // blob tag
926 "152175bf7e5580299fa1f0ba41ef6474cc043b70", // tree tag
927 } {
928 err = w.Checkout(&CheckoutOptions{
929 Hash: plumbing.NewHash(hash),
930 })
931 s.NotNil(err)
932 }
933}
934
935func (s *WorktreeSuite) TestCheckoutBisect() {
936 if testing.Short() {
937 s.T().Skip("skipping test in short mode.")
938 }
939
940 s.testCheckoutBisect("https://github.com/src-d/go-git.git")
941}
942
943func (s *WorktreeSuite) TestCheckoutBisectSubmodules() {
944 s.testCheckoutBisect("https://github.com/git-fixtures/submodule.git")
945}
946
947// TestCheckoutBisect simulates a git bisect going through the git history and
948// checking every commit over the previous commit
949func (s *WorktreeSuite) testCheckoutBisect(url string) {
950 f := fixtures.ByURL(url).One()
951 r := NewRepositoryWithEmptyWorktree(f)
952
953 w, err := r.Worktree()
954 s.NoError(err)
955
956 iter, err := w.r.Log(&LogOptions{})
957 s.NoError(err)
958
959 iter.ForEach(func(commit *object.Commit) error {
960 err := w.Checkout(&CheckoutOptions{Hash: commit.Hash})
961 s.NoError(err)
962
963 status, err := w.Status()
964 s.NoError(err)
965 s.True(status.IsClean())
966
967 return nil
968 })
969}
970
971func (s *WorktreeSuite) TestStatus() {
972 fs := memfs.New()
973 w := &Worktree{
974 r: s.Repository,
975 Filesystem: fs,
976 }
977
978 status, err := w.Status()
979 s.NoError(err)
980
981 s.False(status.IsClean())
982 s.Len(status, 9)
983}
984
985func (s *WorktreeSuite) TestStatusEmpty() {
986 fs := memfs.New()
987 storage := memory.NewStorage()
988
989 r, err := Init(storage, fs)
990 s.NoError(err)
991
992 w, err := r.Worktree()
993 s.NoError(err)
994
995 status, err := w.Status()
996 s.NoError(err)
997 s.True(status.IsClean())
998 s.NotNil(status)
999}
1000
1001func (s *WorktreeSuite) TestStatusCheckedInBeforeIgnored() {
1002 fs := memfs.New()
1003 storage := memory.NewStorage()
1004
1005 r, err := Init(storage, fs)
1006 s.NoError(err)
1007
1008 w, err := r.Worktree()
1009 s.NoError(err)
1010
1011 err = util.WriteFile(fs, "fileToIgnore", []byte("Initial data"), 0o755)
1012 s.NoError(err)
1013 _, err = w.Add("fileToIgnore")
1014 s.NoError(err)
1015
1016 _, err = w.Commit("Added file that will be ignored later", defaultTestCommitOptions())
1017 s.NoError(err)
1018
1019 err = util.WriteFile(fs, ".gitignore", []byte("fileToIgnore\nsecondIgnoredFile"), 0o755)
1020 s.NoError(err)
1021 _, err = w.Add(".gitignore")
1022 s.NoError(err)
1023 _, err = w.Commit("Added .gitignore", defaultTestCommitOptions())
1024 s.NoError(err)
1025 status, err := w.Status()
1026 s.NoError(err)
1027 s.True(status.IsClean())
1028 s.NotNil(status)
1029
1030 err = util.WriteFile(fs, "secondIgnoredFile", []byte("Should be completely ignored"), 0o755)
1031 s.NoError(err)
1032 status = nil
1033 status, err = w.Status()
1034 s.NoError(err)
1035 s.True(status.IsClean())
1036 s.NotNil(status)
1037
1038 err = util.WriteFile(fs, "fileToIgnore", []byte("Updated data"), 0o755)
1039 s.NoError(err)
1040 status = nil
1041 status, err = w.Status()
1042 s.NoError(err)
1043 s.False(status.IsClean())
1044 s.NotNil(status)
1045}
1046
1047func (s *WorktreeSuite) TestStatusEmptyDirty() {
1048 fs := memfs.New()
1049 err := util.WriteFile(fs, "foo", []byte("foo"), 0o755)
1050 s.NoError(err)
1051
1052 storage := memory.NewStorage()
1053
1054 r, err := Init(storage, fs)
1055 s.NoError(err)
1056
1057 w, err := r.Worktree()
1058 s.NoError(err)
1059
1060 status, err := w.Status()
1061 s.NoError(err)
1062 s.False(status.IsClean())
1063 s.Len(status, 1)
1064}
1065
1066func (s *WorktreeSuite) TestStatusUnmodified() {
1067 fs := memfs.New()
1068 w := &Worktree{
1069 r: s.Repository,
1070 Filesystem: fs,
1071 }
1072
1073 err := w.Checkout(&CheckoutOptions{Force: true})
1074 s.NoError(err)
1075
1076 status, err := w.StatusWithOptions(StatusOptions{Strategy: Preload})
1077 s.NoError(err)
1078 s.True(status.IsClean())
1079 s.False(status.IsUntracked("LICENSE"))
1080
1081 s.Equal(Unmodified, status.File("LICENSE").Staging)
1082 s.Equal(Unmodified, status.File("LICENSE").Worktree)
1083
1084 status, err = w.StatusWithOptions(StatusOptions{Strategy: Empty})
1085 s.NoError(err)
1086 s.True(status.IsClean())
1087 s.False(status.IsUntracked("LICENSE"))
1088
1089 s.Equal(Untracked, status.File("LICENSE").Staging)
1090 s.Equal(Untracked, status.File("LICENSE").Worktree)
1091}
1092
1093func (s *WorktreeSuite) TestReset() {
1094 fs := memfs.New()
1095 w := &Worktree{
1096 r: s.Repository,
1097 Filesystem: fs,
1098 }
1099
1100 commit := plumbing.NewHash("35e85108805c84807bc66a02d91535e1e24b38b9")
1101
1102 err := w.Checkout(&CheckoutOptions{})
1103 s.NoError(err)
1104
1105 branch, err := w.r.Reference(plumbing.Master, false)
1106 s.NoError(err)
1107 s.NotEqual(commit, branch.Hash())
1108
1109 err = w.Reset(&ResetOptions{Mode: MergeReset, Commit: commit})
1110 s.NoError(err)
1111
1112 branch, err = w.r.Reference(plumbing.Master, false)
1113 s.NoError(err)
1114 s.Equal(commit, branch.Hash())
1115
1116 status, err := w.Status()
1117 s.NoError(err)
1118 s.True(status.IsClean())
1119}
1120
1121func (s *WorktreeSuite) TestResetWithUntracked() {
1122 fs := memfs.New()
1123 w := &Worktree{
1124 r: s.Repository,
1125 Filesystem: fs,
1126 }
1127
1128 commit := plumbing.NewHash("35e85108805c84807bc66a02d91535e1e24b38b9")
1129
1130 err := w.Checkout(&CheckoutOptions{})
1131 s.NoError(err)
1132
1133 err = util.WriteFile(fs, "foo", nil, 0o755)
1134 s.NoError(err)
1135
1136 err = w.Reset(&ResetOptions{Mode: MergeReset, Commit: commit})
1137 s.NoError(err)
1138
1139 status, err := w.Status()
1140 s.NoError(err)
1141 s.True(status.IsClean())
1142}
1143
1144func (s *WorktreeSuite) TestResetSoft() {
1145 fs := memfs.New()
1146 w := &Worktree{
1147 r: s.Repository,
1148 Filesystem: fs,
1149 }
1150
1151 commit := plumbing.NewHash("35e85108805c84807bc66a02d91535e1e24b38b9")
1152
1153 err := w.Checkout(&CheckoutOptions{})
1154 s.NoError(err)
1155
1156 err = w.Reset(&ResetOptions{Mode: SoftReset, Commit: commit})
1157 s.NoError(err)
1158
1159 branch, err := w.r.Reference(plumbing.Master, false)
1160 s.NoError(err)
1161 s.Equal(commit, branch.Hash())
1162
1163 status, err := w.Status()
1164 s.NoError(err)
1165 s.False(status.IsClean())
1166 s.Equal(Added, status.File("CHANGELOG").Staging)
1167}
1168
1169func (s *WorktreeSuite) TestResetMixed() {
1170 fs := memfs.New()
1171 w := &Worktree{
1172 r: s.Repository,
1173 Filesystem: fs,
1174 }
1175
1176 commit := plumbing.NewHash("35e85108805c84807bc66a02d91535e1e24b38b9")
1177
1178 err := w.Checkout(&CheckoutOptions{})
1179 s.NoError(err)
1180
1181 err = w.Reset(&ResetOptions{Mode: MixedReset, Commit: commit})
1182 s.NoError(err)
1183
1184 branch, err := w.r.Reference(plumbing.Master, false)
1185 s.NoError(err)
1186 s.Equal(commit, branch.Hash())
1187
1188 status, err := w.Status()
1189 s.NoError(err)
1190 s.False(status.IsClean())
1191 s.Equal(Untracked, status.File("CHANGELOG").Staging)
1192}
1193
1194func (s *WorktreeSuite) TestResetMerge() {
1195 fs := memfs.New()
1196 w := &Worktree{
1197 r: s.Repository,
1198 Filesystem: fs,
1199 }
1200
1201 commitA := plumbing.NewHash("918c48b83bd081e863dbe1b80f8998f058cd8294")
1202 commitB := plumbing.NewHash("35e85108805c84807bc66a02d91535e1e24b38b9")
1203
1204 err := w.Checkout(&CheckoutOptions{})
1205 s.NoError(err)
1206
1207 err = w.Reset(&ResetOptions{Mode: MergeReset, Commit: commitA})
1208 s.NoError(err)
1209
1210 branch, err := w.r.Reference(plumbing.Master, false)
1211 s.NoError(err)
1212 s.Equal(commitA, branch.Hash())
1213
1214 f, err := fs.Create(".gitignore")
1215 s.NoError(err)
1216 _, err = f.Write([]byte("foo"))
1217 s.NoError(err)
1218 err = f.Close()
1219 s.NoError(err)
1220
1221 err = w.Reset(&ResetOptions{Mode: MergeReset, Commit: commitB})
1222 s.ErrorIs(err, ErrUnstagedChanges)
1223
1224 branch, err = w.r.Reference(plumbing.Master, false)
1225 s.NoError(err)
1226 s.Equal(commitA, branch.Hash())
1227}
1228
1229func (s *WorktreeSuite) TestResetHard() {
1230 fs := memfs.New()
1231 w := &Worktree{
1232 r: s.Repository,
1233 Filesystem: fs,
1234 }
1235
1236 commit := plumbing.NewHash("35e85108805c84807bc66a02d91535e1e24b38b9")
1237
1238 err := w.Checkout(&CheckoutOptions{})
1239 s.NoError(err)
1240
1241 f, err := fs.Create(".gitignore")
1242 s.NoError(err)
1243 _, err = f.Write([]byte("foo"))
1244 s.NoError(err)
1245 err = f.Close()
1246 s.NoError(err)
1247
1248 err = w.Reset(&ResetOptions{Mode: HardReset, Commit: commit})
1249 s.NoError(err)
1250
1251 branch, err := w.r.Reference(plumbing.Master, false)
1252 s.NoError(err)
1253 s.Equal(commit, branch.Hash())
1254}
1255
1256func (s *WorktreeSuite) TestResetHardSubFolders() {
1257 fs := memfs.New()
1258 w := &Worktree{
1259 r: s.Repository,
1260 Filesystem: fs,
1261 }
1262
1263 err := w.Checkout(&CheckoutOptions{})
1264 s.NoError(err)
1265
1266 err = fs.MkdirAll("dir", os.ModePerm)
1267 s.NoError(err)
1268 tf, err := fs.Create("dir/testfile.txt")
1269 s.NoError(err)
1270 _, err = tf.Write([]byte("testfile content"))
1271 s.NoError(err)
1272 err = tf.Close()
1273 s.NoError(err)
1274 _, err = w.Add("dir/testfile.txt")
1275 s.NoError(err)
1276 _, err = w.Commit("testcommit", &CommitOptions{Author: &object.Signature{Name: "name", Email: "email"}})
1277 s.NoError(err)
1278
1279 err = fs.Remove("dir/testfile.txt")
1280 s.NoError(err)
1281
1282 status, err := w.Status()
1283 s.NoError(err)
1284 s.False(status.IsClean())
1285
1286 err = w.Reset(&ResetOptions{Files: []string{"./dir/testfile.txt"}, Mode: HardReset})
1287 s.NoError(err)
1288
1289 status, err = w.Status()
1290 s.NoError(err)
1291 s.True(status.IsClean())
1292}
1293
1294func (s *WorktreeSuite) TestResetHardWithGitIgnore() {
1295 fs := memfs.New()
1296 w := &Worktree{
1297 r: s.Repository,
1298 Filesystem: fs,
1299 }
1300
1301 err := w.Checkout(&CheckoutOptions{})
1302 s.NoError(err)
1303
1304 tf, err := fs.Create("newTestFile.txt")
1305 s.NoError(err)
1306 _, err = tf.Write([]byte("testfile content"))
1307 s.NoError(err)
1308 err = tf.Close()
1309 s.NoError(err)
1310 _, err = w.Add("newTestFile.txt")
1311 s.NoError(err)
1312 _, err = w.Commit("testcommit", &CommitOptions{Author: &object.Signature{Name: "name", Email: "email"}})
1313 s.NoError(err)
1314
1315 err = fs.Remove("newTestFile.txt")
1316 s.NoError(err)
1317 f, err := fs.Create(".gitignore")
1318 s.NoError(err)
1319 _, err = f.Write([]byte("foo\n"))
1320 s.NoError(err)
1321 _, err = f.Write([]byte("newTestFile.txt\n"))
1322 s.NoError(err)
1323 err = f.Close()
1324 s.NoError(err)
1325
1326 status, err := w.Status()
1327 s.NoError(err)
1328 s.False(status.IsClean())
1329
1330 err = w.Reset(&ResetOptions{Mode: HardReset})
1331 s.NoError(err)
1332
1333 status, err = w.Status()
1334 s.NoError(err)
1335 s.True(status.IsClean())
1336}
1337
1338func (s *WorktreeSuite) TestResetSparsely() {
1339 fs := memfs.New()
1340 w := &Worktree{
1341 r: s.Repository,
1342 Filesystem: fs,
1343 }
1344
1345 sparseResetDirs := []string{"php"}
1346
1347 err := w.ResetSparsely(&ResetOptions{Mode: HardReset}, sparseResetDirs)
1348 s.NoError(err)
1349
1350 files, err := fs.ReadDir("/")
1351 s.NoError(err)
1352 s.Len(files, 1)
1353 s.Equal("php", files[0].Name())
1354
1355 files, err = fs.ReadDir("/php")
1356 s.NoError(err)
1357 s.Len(files, 1)
1358 s.Equal("crappy.php", files[0].Name())
1359}
1360
1361func (s *WorktreeSuite) TestStatusAfterCheckout() {
1362 fs := memfs.New()
1363 w := &Worktree{
1364 r: s.Repository,
1365 Filesystem: fs,
1366 }
1367
1368 err := w.Checkout(&CheckoutOptions{Force: true})
1369 s.NoError(err)
1370
1371 status, err := w.Status()
1372 s.NoError(err)
1373 s.True(status.IsClean())
1374}
1375
1376func (s *WorktreeSuite) TestStatusAfterSparseCheckout() {
1377 fs := memfs.New()
1378 w := &Worktree{
1379 r: s.Repository,
1380 Filesystem: fs,
1381 }
1382
1383 err := w.Checkout(&CheckoutOptions{
1384 SparseCheckoutDirectories: []string{"php"},
1385 Force: true,
1386 })
1387 s.Require().NoError(err)
1388
1389 status, err := w.Status()
1390 s.Require().NoError(err)
1391 s.True(status.IsClean(), status)
1392}
1393
1394func (s *WorktreeSuite) TestStatusModified() {
1395 fs := s.TemporalFilesystem()
1396
1397 w := &Worktree{
1398 r: s.Repository,
1399 Filesystem: fs,
1400 }
1401
1402 err := w.Checkout(&CheckoutOptions{})
1403 s.NoError(err)
1404
1405 f, err := fs.Create(".gitignore")
1406 s.NoError(err)
1407 _, err = f.Write([]byte("foo"))
1408 s.NoError(err)
1409 err = f.Close()
1410 s.NoError(err)
1411
1412 status, err := w.Status()
1413 s.NoError(err)
1414 s.False(status.IsClean())
1415 s.Equal(Modified, status.File(".gitignore").Worktree)
1416}
1417
1418func (s *WorktreeSuite) TestStatusIgnored() {
1419 fs := memfs.New()
1420 w := &Worktree{
1421 r: s.Repository,
1422 Filesystem: fs,
1423 }
1424
1425 w.Checkout(&CheckoutOptions{})
1426
1427 fs.MkdirAll("another", os.ModePerm)
1428 f, _ := fs.Create("another/file")
1429 f.Close()
1430 fs.MkdirAll("vendor/github.com", os.ModePerm)
1431 f, _ = fs.Create("vendor/github.com/file")
1432 f.Close()
1433 fs.MkdirAll("vendor/gopkg.in", os.ModePerm)
1434 f, _ = fs.Create("vendor/gopkg.in/file")
1435 f.Close()
1436
1437 status, _ := w.Status()
1438 s.Len(status, 3)
1439 _, ok := status["another/file"]
1440 s.True(ok)
1441 _, ok = status["vendor/github.com/file"]
1442 s.True(ok)
1443 _, ok = status["vendor/gopkg.in/file"]
1444 s.True(ok)
1445
1446 f, _ = fs.Create(".gitignore")
1447 f.Write([]byte("vendor/g*/"))
1448 f.Close()
1449 f, _ = fs.Create("vendor/.gitignore")
1450 f.Write([]byte("!github.com/\n"))
1451 f.Close()
1452
1453 status, _ = w.Status()
1454 s.Len(status, 4)
1455 _, ok = status[".gitignore"]
1456 s.True(ok)
1457 _, ok = status["another/file"]
1458 s.True(ok)
1459 _, ok = status["vendor/.gitignore"]
1460 s.True(ok)
1461 _, ok = status["vendor/github.com/file"]
1462 s.True(ok)
1463}
1464
1465func (s *WorktreeSuite) TestStatusUntracked() {
1466 fs := memfs.New()
1467 w := &Worktree{
1468 r: s.Repository,
1469 Filesystem: fs,
1470 }
1471
1472 err := w.Checkout(&CheckoutOptions{Force: true})
1473 s.NoError(err)
1474
1475 f, err := w.Filesystem.Create("foo")
1476 s.NoError(err)
1477 s.Nil(f.Close())
1478
1479 status, err := w.Status()
1480 s.NoError(err)
1481 s.Equal(Untracked, status.File("foo").Staging)
1482 s.Equal(Untracked, status.File("foo").Worktree)
1483}
1484
1485func (s *WorktreeSuite) TestStatusDeleted() {
1486 fs := s.TemporalFilesystem()
1487
1488 w := &Worktree{
1489 r: s.Repository,
1490 Filesystem: fs,
1491 }
1492
1493 err := w.Checkout(&CheckoutOptions{})
1494 s.NoError(err)
1495
1496 err = fs.Remove(".gitignore")
1497 s.NoError(err)
1498
1499 status, err := w.Status()
1500 s.NoError(err)
1501 s.False(status.IsClean())
1502 s.Equal(Deleted, status.File(".gitignore").Worktree)
1503}
1504
1505func (s *WorktreeSuite) TestSubmodule() {
1506 path := fixtures.ByTag("submodule").One().Worktree().Root()
1507 r, err := PlainOpen(path)
1508 s.NoError(err)
1509
1510 w, err := r.Worktree()
1511 s.NoError(err)
1512
1513 m, err := w.Submodule("basic")
1514 s.NoError(err)
1515
1516 s.Equal("basic", m.Config().Name)
1517}
1518
1519func (s *WorktreeSuite) TestSubmodules() {
1520 path := fixtures.ByTag("submodule").One().Worktree().Root()
1521 r, err := PlainOpen(path)
1522 s.NoError(err)
1523
1524 w, err := r.Worktree()
1525 s.NoError(err)
1526
1527 l, err := w.Submodules()
1528 s.NoError(err)
1529
1530 s.Len(l, 2)
1531}
1532
1533func (s *WorktreeSuite) TestAddUntracked() {
1534 fs := memfs.New()
1535 w := &Worktree{
1536 r: s.Repository,
1537 Filesystem: fs,
1538 }
1539
1540 err := w.Checkout(&CheckoutOptions{Force: true})
1541 s.NoError(err)
1542
1543 idx, err := w.r.Storer.Index()
1544 s.NoError(err)
1545 s.Len(idx.Entries, 9)
1546
1547 err = util.WriteFile(w.Filesystem, "foo", []byte("FOO"), 0755)
1548 s.NoError(err)
1549
1550 hash, err := w.Add("foo")
1551 s.Equal("d96c7efbfec2814ae0301ad054dc8d9fc416c9b5", hash.String())
1552 s.NoError(err)
1553
1554 idx, err = w.r.Storer.Index()
1555 s.NoError(err)
1556 s.Len(idx.Entries, 10)
1557
1558 e, err := idx.Entry("foo")
1559 s.NoError(err)
1560 s.Equal(hash, e.Hash)
1561 s.Equal(filemode.Executable, e.Mode)
1562
1563 status, err := w.Status()
1564 s.NoError(err)
1565 s.Len(status, 1)
1566
1567 file := status.File("foo")
1568 s.Equal(Added, file.Staging)
1569 s.Equal(Unmodified, file.Worktree)
1570
1571 obj, err := w.r.Storer.EncodedObject(plumbing.BlobObject, hash)
1572 s.NoError(err)
1573 s.NotNil(obj)
1574 s.Equal(int64(3), obj.Size())
1575}
1576
1577func (s *WorktreeSuite) TestIgnored() {
1578 fs := memfs.New()
1579 w := &Worktree{
1580 r: s.Repository,
1581 Filesystem: fs,
1582 }
1583
1584 w.Excludes = make([]gitignore.Pattern, 0)
1585 w.Excludes = append(w.Excludes, gitignore.ParsePattern("foo", nil))
1586
1587 err := w.Checkout(&CheckoutOptions{Force: true})
1588 s.NoError(err)
1589
1590 idx, err := w.r.Storer.Index()
1591 s.NoError(err)
1592 s.Len(idx.Entries, 9)
1593
1594 err = util.WriteFile(w.Filesystem, "foo", []byte("FOO"), 0o755)
1595 s.NoError(err)
1596
1597 status, err := w.Status()
1598 s.NoError(err)
1599 s.Len(status, 0)
1600
1601 file := status.File("foo")
1602 s.Equal(Untracked, file.Staging)
1603 s.Equal(Untracked, file.Worktree)
1604}
1605
1606func (s *WorktreeSuite) TestExcludedNoGitignore() {
1607 f := fixtures.ByTag("empty").One()
1608 r := s.NewRepository(f)
1609
1610 fs := memfs.New()
1611 w := &Worktree{
1612 r: r,
1613 Filesystem: fs,
1614 }
1615
1616 _, err := fs.Open(".gitignore")
1617 s.ErrorIs(err, os.ErrNotExist)
1618
1619 w.Excludes = make([]gitignore.Pattern, 0)
1620 w.Excludes = append(w.Excludes, gitignore.ParsePattern("foo", nil))
1621
1622 err = util.WriteFile(w.Filesystem, "foo", []byte("FOO"), 0o755)
1623 s.NoError(err)
1624
1625 status, err := w.Status()
1626 s.NoError(err)
1627 s.Len(status, 0)
1628
1629 file := status.File("foo")
1630 s.Equal(Untracked, file.Staging)
1631 s.Equal(Untracked, file.Worktree)
1632}
1633
1634func (s *WorktreeSuite) TestAddModified() {
1635 fs := memfs.New()
1636 w := &Worktree{
1637 r: s.Repository,
1638 Filesystem: fs,
1639 }
1640
1641 err := w.Checkout(&CheckoutOptions{Force: true})
1642 s.NoError(err)
1643
1644 idx, err := w.r.Storer.Index()
1645 s.NoError(err)
1646 s.Len(idx.Entries, 9)
1647
1648 err = util.WriteFile(w.Filesystem, "LICENSE", []byte("FOO"), 0o644)
1649 s.NoError(err)
1650
1651 hash, err := w.Add("LICENSE")
1652 s.NoError(err)
1653 s.Equal("d96c7efbfec2814ae0301ad054dc8d9fc416c9b5", hash.String())
1654
1655 idx, err = w.r.Storer.Index()
1656 s.NoError(err)
1657 s.Len(idx.Entries, 9)
1658
1659 e, err := idx.Entry("LICENSE")
1660 s.NoError(err)
1661 s.Equal(hash, e.Hash)
1662 s.Equal(filemode.Regular, e.Mode)
1663
1664 status, err := w.Status()
1665 s.NoError(err)
1666 s.Len(status, 1)
1667
1668 file := status.File("LICENSE")
1669 s.Equal(Modified, file.Staging)
1670 s.Equal(Unmodified, file.Worktree)
1671}
1672
1673func (s *WorktreeSuite) TestAddUnmodified() {
1674 fs := memfs.New()
1675 w := &Worktree{
1676 r: s.Repository,
1677 Filesystem: fs,
1678 }
1679
1680 err := w.Checkout(&CheckoutOptions{Force: true})
1681 s.NoError(err)
1682
1683 hash, err := w.Add("LICENSE")
1684 s.Equal("c192bd6a24ea1ab01d78686e417c8bdc7c3d197f", hash.String())
1685 s.NoError(err)
1686}
1687
1688func (s *WorktreeSuite) TestAddRemoved() {
1689 fs := memfs.New()
1690 w := &Worktree{
1691 r: s.Repository,
1692 Filesystem: fs,
1693 }
1694
1695 err := w.Checkout(&CheckoutOptions{Force: true})
1696 s.NoError(err)
1697
1698 idx, err := w.r.Storer.Index()
1699 s.NoError(err)
1700 s.Len(idx.Entries, 9)
1701
1702 err = w.Filesystem.Remove("LICENSE")
1703 s.NoError(err)
1704
1705 hash, err := w.Add("LICENSE")
1706 s.NoError(err)
1707 s.Equal("c192bd6a24ea1ab01d78686e417c8bdc7c3d197f", hash.String())
1708
1709 e, err := idx.Entry("LICENSE")
1710 s.NoError(err)
1711 s.Equal(hash, e.Hash)
1712 s.Equal(filemode.Regular, e.Mode)
1713
1714 status, err := w.Status()
1715 s.NoError(err)
1716 s.Len(status, 1)
1717
1718 file := status.File("LICENSE")
1719 s.Equal(Deleted, file.Staging)
1720}
1721
1722func (s *WorktreeSuite) TestAddRemovedInDirectory() {
1723 fs := memfs.New()
1724 w := &Worktree{
1725 r: s.Repository,
1726 Filesystem: fs,
1727 }
1728
1729 err := w.Checkout(&CheckoutOptions{Force: true})
1730 s.NoError(err)
1731
1732 idx, err := w.r.Storer.Index()
1733 s.NoError(err)
1734 s.Len(idx.Entries, 9)
1735
1736 err = w.Filesystem.Remove("go/example.go")
1737 s.NoError(err)
1738
1739 err = w.Filesystem.Remove("json/short.json")
1740 s.NoError(err)
1741
1742 hash, err := w.Add("go")
1743 s.NoError(err)
1744 s.True(hash.IsZero())
1745
1746 e, err := idx.Entry("go/example.go")
1747 s.NoError(err)
1748 s.Equal(plumbing.NewHash("880cd14280f4b9b6ed3986d6671f907d7cc2a198"), e.Hash)
1749 s.Equal(filemode.Regular, e.Mode)
1750
1751 e, err = idx.Entry("json/short.json")
1752 s.NoError(err)
1753 s.Equal(plumbing.NewHash("c8f1d8c61f9da76f4cb49fd86322b6e685dba956"), e.Hash)
1754 s.Equal(filemode.Regular, e.Mode)
1755
1756 status, err := w.Status()
1757 s.NoError(err)
1758 s.Len(status, 2)
1759
1760 file := status.File("go/example.go")
1761 s.Equal(Deleted, file.Staging)
1762
1763 file = status.File("json/short.json")
1764 s.Equal(Unmodified, file.Staging)
1765}
1766
1767func (s *WorktreeSuite) TestAddRemovedInDirectoryWithTrailingSlash() {
1768 fs := memfs.New()
1769 w := &Worktree{
1770 r: s.Repository,
1771 Filesystem: fs,
1772 }
1773
1774 err := w.Checkout(&CheckoutOptions{Force: true})
1775 s.NoError(err)
1776
1777 idx, err := w.r.Storer.Index()
1778 s.NoError(err)
1779 s.Len(idx.Entries, 9)
1780
1781 err = w.Filesystem.Remove("go/example.go")
1782 s.NoError(err)
1783
1784 err = w.Filesystem.Remove("json/short.json")
1785 s.NoError(err)
1786
1787 hash, err := w.Add("go/")
1788 s.NoError(err)
1789 s.True(hash.IsZero())
1790
1791 e, err := idx.Entry("go/example.go")
1792 s.NoError(err)
1793 s.Equal(plumbing.NewHash("880cd14280f4b9b6ed3986d6671f907d7cc2a198"), e.Hash)
1794 s.Equal(filemode.Regular, e.Mode)
1795
1796 e, err = idx.Entry("json/short.json")
1797 s.NoError(err)
1798 s.Equal(plumbing.NewHash("c8f1d8c61f9da76f4cb49fd86322b6e685dba956"), e.Hash)
1799 s.Equal(filemode.Regular, e.Mode)
1800
1801 status, err := w.Status()
1802 s.NoError(err)
1803 s.Len(status, 2)
1804
1805 file := status.File("go/example.go")
1806 s.Equal(Deleted, file.Staging)
1807
1808 file = status.File("json/short.json")
1809 s.Equal(Unmodified, file.Staging)
1810}
1811
1812func (s *WorktreeSuite) TestAddRemovedInDirectoryDot() {
1813 fs := memfs.New()
1814 w := &Worktree{
1815 r: s.Repository,
1816 Filesystem: fs,
1817 }
1818
1819 err := w.Checkout(&CheckoutOptions{Force: true})
1820 s.NoError(err)
1821
1822 idx, err := w.r.Storer.Index()
1823 s.NoError(err)
1824 s.Len(idx.Entries, 9)
1825
1826 err = w.Filesystem.Remove("go/example.go")
1827 s.NoError(err)
1828
1829 err = w.Filesystem.Remove("json/short.json")
1830 s.NoError(err)
1831
1832 hash, err := w.Add(".")
1833 s.NoError(err)
1834 s.True(hash.IsZero())
1835
1836 e, err := idx.Entry("go/example.go")
1837 s.NoError(err)
1838 s.Equal(plumbing.NewHash("880cd14280f4b9b6ed3986d6671f907d7cc2a198"), e.Hash)
1839 s.Equal(filemode.Regular, e.Mode)
1840
1841 e, err = idx.Entry("json/short.json")
1842 s.NoError(err)
1843 s.Equal(plumbing.NewHash("c8f1d8c61f9da76f4cb49fd86322b6e685dba956"), e.Hash)
1844 s.Equal(filemode.Regular, e.Mode)
1845
1846 status, err := w.Status()
1847 s.NoError(err)
1848 s.Len(status, 2)
1849
1850 file := status.File("go/example.go")
1851 s.Equal(Deleted, file.Staging)
1852
1853 file = status.File("json/short.json")
1854 s.Equal(Deleted, file.Staging)
1855}
1856
1857func (s *WorktreeSuite) TestAddSymlink() {
1858 dir, err := os.MkdirTemp("", "")
1859 s.NoError(err)
1860
1861 r, err := PlainInit(dir, false)
1862 s.NoError(err)
1863 err = util.WriteFile(r.wt, "foo", []byte("qux"), 0o644)
1864 s.NoError(err)
1865 err = r.wt.Symlink("foo", "bar")
1866 s.NoError(err)
1867
1868 w, err := r.Worktree()
1869 s.NoError(err)
1870 h, err := w.Add("foo")
1871 s.NoError(err)
1872 s.NotEqual(plumbing.NewHash("19102815663d23f8b75a47e7a01965dcdc96468c"), h)
1873
1874 h, err = w.Add("bar")
1875 s.NoError(err)
1876 s.Equal(plumbing.NewHash("19102815663d23f8b75a47e7a01965dcdc96468c"), h)
1877
1878 obj, err := w.r.Storer.EncodedObject(plumbing.BlobObject, h)
1879 s.NoError(err)
1880 s.NotNil(obj)
1881 s.Equal(int64(3), obj.Size())
1882}
1883
1884func (s *WorktreeSuite) TestAddDirectory() {
1885 fs := memfs.New()
1886 w := &Worktree{
1887 r: s.Repository,
1888 Filesystem: fs,
1889 }
1890
1891 err := w.Checkout(&CheckoutOptions{Force: true})
1892 s.NoError(err)
1893
1894 idx, err := w.r.Storer.Index()
1895 s.NoError(err)
1896 s.Len(idx.Entries, 9)
1897
1898 err = util.WriteFile(w.Filesystem, "qux/foo", []byte("FOO"), 0o755)
1899 s.NoError(err)
1900 err = util.WriteFile(w.Filesystem, "qux/baz/bar", []byte("BAR"), 0o755)
1901 s.NoError(err)
1902
1903 h, err := w.Add("qux")
1904 s.NoError(err)
1905 s.True(h.IsZero())
1906
1907 idx, err = w.r.Storer.Index()
1908 s.NoError(err)
1909 s.Len(idx.Entries, 11)
1910
1911 e, err := idx.Entry("qux/foo")
1912 s.NoError(err)
1913 s.Equal(filemode.Executable, e.Mode)
1914
1915 e, err = idx.Entry("qux/baz/bar")
1916 s.NoError(err)
1917 s.Equal(filemode.Executable, e.Mode)
1918
1919 status, err := w.Status()
1920 s.NoError(err)
1921 s.Len(status, 2)
1922
1923 file := status.File("qux/foo")
1924 s.Equal(Added, file.Staging)
1925 s.Equal(Unmodified, file.Worktree)
1926
1927 file = status.File("qux/baz/bar")
1928 s.Equal(Added, file.Staging)
1929 s.Equal(Unmodified, file.Worktree)
1930}
1931
1932func (s *WorktreeSuite) TestAddDirectoryErrorNotFound() {
1933 r, _ := Init(memory.NewStorage(), memfs.New())
1934 w, _ := r.Worktree()
1935
1936 h, err := w.Add("foo")
1937 s.NotNil(err)
1938 s.True(h.IsZero())
1939}
1940
1941func (s *WorktreeSuite) TestAddAll() {
1942 fs := memfs.New()
1943 w := &Worktree{
1944 r: s.Repository,
1945 Filesystem: fs,
1946 }
1947
1948 err := w.Checkout(&CheckoutOptions{Force: true})
1949 s.NoError(err)
1950
1951 idx, err := w.r.Storer.Index()
1952 s.NoError(err)
1953 s.Len(idx.Entries, 9)
1954
1955 err = util.WriteFile(w.Filesystem, "file1", []byte("file1"), 0o644)
1956 s.NoError(err)
1957
1958 err = util.WriteFile(w.Filesystem, "file2", []byte("file2"), 0o644)
1959 s.NoError(err)
1960
1961 err = util.WriteFile(w.Filesystem, "file3", []byte("ignore me"), 0o644)
1962 s.NoError(err)
1963
1964 w.Excludes = make([]gitignore.Pattern, 0)
1965 w.Excludes = append(w.Excludes, gitignore.ParsePattern("file3", nil))
1966
1967 err = w.AddWithOptions(&AddOptions{All: true})
1968 s.NoError(err)
1969
1970 idx, err = w.r.Storer.Index()
1971 s.NoError(err)
1972 s.Len(idx.Entries, 11)
1973
1974 status, err := w.Status()
1975 s.NoError(err)
1976 s.Len(status, 2)
1977
1978 file1 := status.File("file1")
1979 s.Equal(Added, file1.Staging)
1980 file2 := status.File("file2")
1981 s.Equal(Added, file2.Staging)
1982 file3 := status.File("file3")
1983 s.Equal(Untracked, file3.Staging)
1984 s.Equal(Untracked, file3.Worktree)
1985}
1986
1987func (s *WorktreeSuite) TestAddGlob() {
1988 fs := memfs.New()
1989 w := &Worktree{
1990 r: s.Repository,
1991 Filesystem: fs,
1992 }
1993
1994 err := w.Checkout(&CheckoutOptions{Force: true})
1995 s.NoError(err)
1996
1997 idx, err := w.r.Storer.Index()
1998 s.NoError(err)
1999 s.Len(idx.Entries, 9)
2000
2001 err = util.WriteFile(w.Filesystem, "qux/qux", []byte("QUX"), 0o755)
2002 s.NoError(err)
2003 err = util.WriteFile(w.Filesystem, "qux/baz", []byte("BAZ"), 0o755)
2004 s.NoError(err)
2005 err = util.WriteFile(w.Filesystem, "qux/bar/baz", []byte("BAZ"), 0o755)
2006 s.NoError(err)
2007
2008 err = w.AddWithOptions(&AddOptions{Glob: w.Filesystem.Join("qux", "b*")})
2009 s.NoError(err)
2010
2011 idx, err = w.r.Storer.Index()
2012 s.NoError(err)
2013 s.Len(idx.Entries, 11)
2014
2015 e, err := idx.Entry("qux/baz")
2016 s.NoError(err)
2017 s.Equal(filemode.Executable, e.Mode)
2018
2019 e, err = idx.Entry("qux/bar/baz")
2020 s.NoError(err)
2021 s.Equal(filemode.Executable, e.Mode)
2022
2023 status, err := w.Status()
2024 s.NoError(err)
2025 s.Len(status, 3)
2026
2027 file := status.File("qux/qux")
2028 s.Equal(Untracked, file.Staging)
2029 s.Equal(Untracked, file.Worktree)
2030
2031 file = status.File("qux/baz")
2032 s.Equal(Added, file.Staging)
2033 s.Equal(Unmodified, file.Worktree)
2034
2035 file = status.File("qux/bar/baz")
2036 s.Equal(Added, file.Staging)
2037 s.Equal(Unmodified, file.Worktree)
2038}
2039
2040func (s *WorktreeSuite) TestAddFilenameStartingWithDot() {
2041 fs := memfs.New()
2042 w := &Worktree{
2043 r: s.Repository,
2044 Filesystem: fs,
2045 }
2046
2047 err := w.Checkout(&CheckoutOptions{Force: true})
2048 s.NoError(err)
2049
2050 idx, err := w.r.Storer.Index()
2051 s.NoError(err)
2052 s.Len(idx.Entries, 9)
2053
2054 err = util.WriteFile(w.Filesystem, "qux", []byte("QUX"), 0o755)
2055 s.NoError(err)
2056 err = util.WriteFile(w.Filesystem, "baz", []byte("BAZ"), 0o755)
2057 s.NoError(err)
2058 err = util.WriteFile(w.Filesystem, "foo/bar/baz", []byte("BAZ"), 0o755)
2059 s.NoError(err)
2060
2061 _, err = w.Add("./qux")
2062 s.NoError(err)
2063
2064 _, err = w.Add("./baz")
2065 s.NoError(err)
2066
2067 _, err = w.Add("foo/bar/../bar/./baz")
2068 s.NoError(err)
2069
2070 idx, err = w.r.Storer.Index()
2071 s.NoError(err)
2072 s.Len(idx.Entries, 12)
2073
2074 e, err := idx.Entry("qux")
2075 s.NoError(err)
2076 s.Equal(filemode.Executable, e.Mode)
2077
2078 e, err = idx.Entry("baz")
2079 s.NoError(err)
2080 s.Equal(filemode.Executable, e.Mode)
2081
2082 status, err := w.Status()
2083 s.NoError(err)
2084 s.Len(status, 3)
2085
2086 file := status.File("qux")
2087 s.Equal(Added, file.Staging)
2088 s.Equal(Unmodified, file.Worktree)
2089
2090 file = status.File("baz")
2091 s.Equal(Added, file.Staging)
2092 s.Equal(Unmodified, file.Worktree)
2093
2094 file = status.File("foo/bar/baz")
2095 s.Equal(Added, file.Staging)
2096 s.Equal(Unmodified, file.Worktree)
2097}
2098
2099func (s *WorktreeSuite) TestAddGlobErrorNoMatches() {
2100 r, _ := Init(memory.NewStorage(), memfs.New())
2101 w, _ := r.Worktree()
2102
2103 err := w.AddGlob("foo")
2104 s.ErrorIs(err, ErrGlobNoMatches)
2105}
2106
2107func (s *WorktreeSuite) TestAddSkipStatusAddedPath() {
2108 fs := memfs.New()
2109 w := &Worktree{
2110 r: s.Repository,
2111 Filesystem: fs,
2112 }
2113
2114 err := w.Checkout(&CheckoutOptions{Force: true})
2115 s.NoError(err)
2116
2117 idx, err := w.r.Storer.Index()
2118 s.NoError(err)
2119 s.Len(idx.Entries, 9)
2120
2121 err = util.WriteFile(w.Filesystem, "file1", []byte("file1"), 0o644)
2122 s.NoError(err)
2123
2124 err = w.AddWithOptions(&AddOptions{Path: "file1", SkipStatus: true})
2125 s.NoError(err)
2126
2127 idx, err = w.r.Storer.Index()
2128 s.NoError(err)
2129 s.Len(idx.Entries, 10)
2130
2131 e, err := idx.Entry("file1")
2132 s.NoError(err)
2133 s.Equal(filemode.Regular, e.Mode)
2134
2135 status, err := w.Status()
2136 s.NoError(err)
2137 s.Len(status, 1)
2138
2139 file := status.File("file1")
2140 s.Equal(Added, file.Staging)
2141 s.Equal(Unmodified, file.Worktree)
2142}
2143
2144func (s *WorktreeSuite) TestAddSkipStatusModifiedPath() {
2145 fs := memfs.New()
2146 w := &Worktree{
2147 r: s.Repository,
2148 Filesystem: fs,
2149 }
2150
2151 err := w.Checkout(&CheckoutOptions{Force: true})
2152 s.NoError(err)
2153
2154 idx, err := w.r.Storer.Index()
2155 s.NoError(err)
2156 s.Len(idx.Entries, 9)
2157
2158 err = util.WriteFile(w.Filesystem, "LICENSE", []byte("file1"), 0o644)
2159 s.NoError(err)
2160
2161 err = w.AddWithOptions(&AddOptions{Path: "LICENSE", SkipStatus: true})
2162 s.NoError(err)
2163
2164 idx, err = w.r.Storer.Index()
2165 s.NoError(err)
2166 s.Len(idx.Entries, 9)
2167
2168 e, err := idx.Entry("LICENSE")
2169 s.NoError(err)
2170 s.Equal(filemode.Regular, e.Mode)
2171
2172 status, err := w.Status()
2173 s.NoError(err)
2174 s.Len(status, 1)
2175
2176 file := status.File("LICENSE")
2177 s.Equal(Modified, file.Staging)
2178 s.Equal(Unmodified, file.Worktree)
2179}
2180
2181func (s *WorktreeSuite) TestAddSkipStatusNonModifiedPath() {
2182 fs := memfs.New()
2183 w := &Worktree{
2184 r: s.Repository,
2185 Filesystem: fs,
2186 }
2187
2188 err := w.Checkout(&CheckoutOptions{Force: true})
2189 s.NoError(err)
2190
2191 idx, err := w.r.Storer.Index()
2192 s.NoError(err)
2193 s.Len(idx.Entries, 9)
2194
2195 err = w.AddWithOptions(&AddOptions{Path: "LICENSE", SkipStatus: true})
2196 s.NoError(err)
2197
2198 idx, err = w.r.Storer.Index()
2199 s.NoError(err)
2200 s.Len(idx.Entries, 9)
2201
2202 e, err := idx.Entry("LICENSE")
2203 s.NoError(err)
2204 s.Equal(filemode.Regular, e.Mode)
2205
2206 status, err := w.Status()
2207 s.NoError(err)
2208 s.Len(status, 0)
2209
2210 file := status.File("LICENSE")
2211 s.Equal(Untracked, file.Staging)
2212 s.Equal(Untracked, file.Worktree)
2213}
2214
2215func (s *WorktreeSuite) TestAddSkipStatusWithIgnoredPath() {
2216 fs := memfs.New()
2217 w := &Worktree{
2218 r: s.Repository,
2219 Filesystem: fs,
2220 }
2221
2222 err := w.Checkout(&CheckoutOptions{Force: true})
2223 s.NoError(err)
2224
2225 idx, err := w.r.Storer.Index()
2226 s.NoError(err)
2227 s.Len(idx.Entries, 9)
2228
2229 err = util.WriteFile(fs, ".gitignore", []byte("fileToIgnore\n"), 0o755)
2230 s.NoError(err)
2231 _, err = w.Add(".gitignore")
2232 s.NoError(err)
2233 _, err = w.Commit("Added .gitignore", defaultTestCommitOptions())
2234 s.NoError(err)
2235
2236 err = util.WriteFile(fs, "fileToIgnore", []byte("file to ignore"), 0o644)
2237 s.NoError(err)
2238
2239 status, err := w.Status()
2240 s.NoError(err)
2241 s.Len(status, 0)
2242
2243 file := status.File("fileToIgnore")
2244 s.Equal(Untracked, file.Staging)
2245 s.Equal(Untracked, file.Worktree)
2246
2247 err = w.AddWithOptions(&AddOptions{Path: "fileToIgnore", SkipStatus: true})
2248 s.NoError(err)
2249
2250 idx, err = w.r.Storer.Index()
2251 s.NoError(err)
2252 s.Len(idx.Entries, 10)
2253
2254 e, err := idx.Entry("fileToIgnore")
2255 s.NoError(err)
2256 s.Equal(filemode.Regular, e.Mode)
2257
2258 status, err = w.Status()
2259 s.NoError(err)
2260 s.Len(status, 1)
2261
2262 file = status.File("fileToIgnore")
2263 s.Equal(Added, file.Staging)
2264 s.Equal(Unmodified, file.Worktree)
2265}
2266
2267func (s *WorktreeSuite) TestRemove() {
2268 fs := memfs.New()
2269 w := &Worktree{
2270 r: s.Repository,
2271 Filesystem: fs,
2272 }
2273
2274 err := w.Checkout(&CheckoutOptions{Force: true})
2275 s.NoError(err)
2276
2277 hash, err := w.Remove("LICENSE")
2278 s.Equal("c192bd6a24ea1ab01d78686e417c8bdc7c3d197f", hash.String())
2279 s.NoError(err)
2280
2281 status, err := w.Status()
2282 s.NoError(err)
2283 s.Len(status, 1)
2284 s.Equal(Deleted, status.File("LICENSE").Staging)
2285}
2286
2287func (s *WorktreeSuite) TestRemoveNotExistentEntry() {
2288 fs := memfs.New()
2289 w := &Worktree{
2290 r: s.Repository,
2291 Filesystem: fs,
2292 }
2293
2294 err := w.Checkout(&CheckoutOptions{Force: true})
2295 s.NoError(err)
2296
2297 hash, err := w.Remove("not-exists")
2298 s.True(hash.IsZero())
2299 s.NotNil(err)
2300}
2301
2302func (s *WorktreeSuite) TestRemoveDirectory() {
2303 fs := memfs.New()
2304 w := &Worktree{
2305 r: s.Repository,
2306 Filesystem: fs,
2307 }
2308
2309 err := w.Checkout(&CheckoutOptions{Force: true})
2310 s.NoError(err)
2311
2312 hash, err := w.Remove("json")
2313 s.True(hash.IsZero())
2314 s.NoError(err)
2315
2316 status, err := w.Status()
2317 s.NoError(err)
2318 s.Len(status, 2)
2319 s.Equal(Deleted, status.File("json/long.json").Staging)
2320 s.Equal(Deleted, status.File("json/short.json").Staging)
2321
2322 _, err = w.Filesystem.Stat("json")
2323 s.True(os.IsNotExist(err))
2324}
2325
2326func (s *WorktreeSuite) TestRemoveDirectoryUntracked() {
2327 fs := memfs.New()
2328 w := &Worktree{
2329 r: s.Repository,
2330 Filesystem: fs,
2331 }
2332
2333 err := w.Checkout(&CheckoutOptions{Force: true})
2334 s.NoError(err)
2335
2336 err = util.WriteFile(w.Filesystem, "json/foo", []byte("FOO"), 0o755)
2337 s.NoError(err)
2338
2339 hash, err := w.Remove("json")
2340 s.True(hash.IsZero())
2341 s.NoError(err)
2342
2343 status, err := w.Status()
2344 s.NoError(err)
2345 s.Len(status, 3)
2346 s.Equal(Deleted, status.File("json/long.json").Staging)
2347 s.Equal(Deleted, status.File("json/short.json").Staging)
2348 s.Equal(Untracked, status.File("json/foo").Staging)
2349
2350 _, err = w.Filesystem.Stat("json")
2351 s.NoError(err)
2352}
2353
2354func (s *WorktreeSuite) TestRemoveDeletedFromWorktree() {
2355 fs := memfs.New()
2356 w := &Worktree{
2357 r: s.Repository,
2358 Filesystem: fs,
2359 }
2360
2361 err := w.Checkout(&CheckoutOptions{Force: true})
2362 s.NoError(err)
2363
2364 err = fs.Remove("LICENSE")
2365 s.NoError(err)
2366
2367 hash, err := w.Remove("LICENSE")
2368 s.Equal("c192bd6a24ea1ab01d78686e417c8bdc7c3d197f", hash.String())
2369 s.NoError(err)
2370
2371 status, err := w.Status()
2372 s.NoError(err)
2373 s.Len(status, 1)
2374 s.Equal(Deleted, status.File("LICENSE").Staging)
2375}
2376
2377func (s *WorktreeSuite) TestRemoveGlob() {
2378 fs := memfs.New()
2379 w := &Worktree{
2380 r: s.Repository,
2381 Filesystem: fs,
2382 }
2383
2384 err := w.Checkout(&CheckoutOptions{Force: true})
2385 s.NoError(err)
2386
2387 err = w.RemoveGlob(w.Filesystem.Join("json", "l*"))
2388 s.NoError(err)
2389
2390 status, err := w.Status()
2391 s.NoError(err)
2392 s.Len(status, 1)
2393 s.Equal(Deleted, status.File("json/long.json").Staging)
2394}
2395
2396func (s *WorktreeSuite) TestRemoveGlobDirectory() {
2397 fs := memfs.New()
2398 w := &Worktree{
2399 r: s.Repository,
2400 Filesystem: fs,
2401 }
2402
2403 err := w.Checkout(&CheckoutOptions{Force: true})
2404 s.NoError(err)
2405
2406 err = w.RemoveGlob("js*")
2407 s.NoError(err)
2408
2409 status, err := w.Status()
2410 s.NoError(err)
2411 s.Len(status, 2)
2412 s.Equal(Deleted, status.File("json/short.json").Staging)
2413 s.Equal(Deleted, status.File("json/long.json").Staging)
2414
2415 _, err = w.Filesystem.Stat("json")
2416 s.True(os.IsNotExist(err))
2417}
2418
2419func (s *WorktreeSuite) TestRemoveGlobDirectoryDeleted() {
2420 fs := memfs.New()
2421 w := &Worktree{
2422 r: s.Repository,
2423 Filesystem: fs,
2424 }
2425
2426 err := w.Checkout(&CheckoutOptions{Force: true})
2427 s.NoError(err)
2428
2429 err = fs.Remove("json/short.json")
2430 s.NoError(err)
2431
2432 err = util.WriteFile(w.Filesystem, "json/foo", []byte("FOO"), 0o755)
2433 s.NoError(err)
2434
2435 err = w.RemoveGlob("js*")
2436 s.NoError(err)
2437
2438 status, err := w.Status()
2439 s.NoError(err)
2440 s.Len(status, 3)
2441 s.Equal(Deleted, status.File("json/short.json").Staging)
2442 s.Equal(Deleted, status.File("json/long.json").Staging)
2443}
2444
2445func (s *WorktreeSuite) TestMove() {
2446 fs := memfs.New()
2447 w := &Worktree{
2448 r: s.Repository,
2449 Filesystem: fs,
2450 }
2451
2452 err := w.Checkout(&CheckoutOptions{Force: true})
2453 s.NoError(err)
2454
2455 hash, err := w.Move("LICENSE", "foo")
2456 s.Equal("c192bd6a24ea1ab01d78686e417c8bdc7c3d197f", hash.String())
2457 s.NoError(err)
2458
2459 status, err := w.Status()
2460 s.NoError(err)
2461 s.Len(status, 2)
2462 s.Equal(Deleted, status.File("LICENSE").Staging)
2463 s.Equal(Added, status.File("foo").Staging)
2464}
2465
2466func (s *WorktreeSuite) TestMoveNotExistentEntry() {
2467 fs := memfs.New()
2468 w := &Worktree{
2469 r: s.Repository,
2470 Filesystem: fs,
2471 }
2472
2473 err := w.Checkout(&CheckoutOptions{Force: true})
2474 s.NoError(err)
2475
2476 hash, err := w.Move("not-exists", "foo")
2477 s.True(hash.IsZero())
2478 s.NotNil(err)
2479}
2480
2481func (s *WorktreeSuite) TestMoveToExistent() {
2482 fs := memfs.New()
2483 w := &Worktree{
2484 r: s.Repository,
2485 Filesystem: fs,
2486 }
2487
2488 err := w.Checkout(&CheckoutOptions{Force: true})
2489 s.NoError(err)
2490
2491 hash, err := w.Move(".gitignore", "LICENSE")
2492 s.True(hash.IsZero())
2493 s.ErrorIs(err, ErrDestinationExists)
2494}
2495
2496func (s *WorktreeSuite) TestClean() {
2497 fs := fixtures.ByTag("dirty").One().Worktree()
2498
2499 // Open the repo.
2500 fs, err := fs.Chroot("repo")
2501 s.NoError(err)
2502 r, err := PlainOpen(fs.Root())
2503 s.NoError(err)
2504
2505 wt, err := r.Worktree()
2506 s.NoError(err)
2507
2508 // Status before cleaning.
2509 status, err := wt.Status()
2510 s.NoError(err)
2511 s.Len(status, 2)
2512
2513 err = wt.Clean(&CleanOptions{})
2514 s.NoError(err)
2515
2516 // Status after cleaning.
2517 status, err = wt.Status()
2518 s.NoError(err)
2519
2520 s.Len(status, 1)
2521
2522 fi, err := fs.Lstat("pkgA")
2523 s.NoError(err)
2524 s.True(fi.IsDir())
2525
2526 // Clean with Dir: true.
2527 err = wt.Clean(&CleanOptions{Dir: true})
2528 s.NoError(err)
2529
2530 status, err = wt.Status()
2531 s.NoError(err)
2532
2533 s.Len(status, 0)
2534
2535 // An empty dir should be deleted, as well.
2536 _, err = fs.Lstat("pkgA")
2537 s.ErrorIs(err, os.ErrNotExist)
2538}
2539
2540func (s *WorktreeSuite) TestCleanBare() {
2541 storer := memory.NewStorage()
2542
2543 r, err := Init(storer, nil)
2544 s.NoError(err)
2545 s.NotNil(r)
2546
2547 wtfs := memfs.New()
2548
2549 err = wtfs.MkdirAll("worktree", os.ModePerm)
2550 s.NoError(err)
2551
2552 wtfs, err = wtfs.Chroot("worktree")
2553 s.NoError(err)
2554
2555 r, err = Open(storer, wtfs)
2556 s.NoError(err)
2557
2558 wt, err := r.Worktree()
2559 s.NoError(err)
2560
2561 _, err = wt.Filesystem.Lstat(".")
2562 s.NoError(err)
2563
2564 // Clean with Dir: true.
2565 err = wt.Clean(&CleanOptions{Dir: true})
2566 s.NoError(err)
2567
2568 // Root worktree directory must remain after cleaning
2569 _, err = wt.Filesystem.Lstat(".")
2570 s.NoError(err)
2571}
2572
2573func TestAlternatesRepo(t *testing.T) {
2574 fs := fixtures.ByTag("alternates").One().Worktree()
2575
2576 // Open 1st repo.
2577 rep1fs, err := fs.Chroot("rep1")
2578 assert.NoError(t, err)
2579 rep1, err := PlainOpen(rep1fs.Root())
2580 assert.NoError(t, err)
2581
2582 // Open 2nd repo.
2583 rep2fs, err := fs.Chroot("rep2")
2584 assert.NoError(t, err)
2585 d, _ := rep2fs.Chroot(GitDirName)
2586 storer := filesystem.NewStorageWithOptions(d,
2587 cache.NewObjectLRUDefault(), filesystem.Options{
2588 AlternatesFS: fs,
2589 })
2590 rep2, err := Open(storer, rep2fs)
2591
2592 assert.NoError(t, err)
2593
2594 // Get the HEAD commit from the main repo.
2595 h, err := rep1.Head()
2596 assert.NoError(t, err)
2597 commit1, err := rep1.CommitObject(h.Hash())
2598 assert.NoError(t, err)
2599
2600 // Get the HEAD commit from the shared repo.
2601 h, err = rep2.Head()
2602 assert.NoError(t, err)
2603 commit2, err := rep2.CommitObject(h.Hash())
2604 assert.NoError(t, err)
2605
2606 assert.Equal(t, commit1.String(), commit2.String())
2607}
2608
2609func (s *WorktreeSuite) TestGrep() {
2610 cases := []struct {
2611 name string
2612 options GrepOptions
2613 wantResult []GrepResult
2614 dontWantResult []GrepResult
2615 wantError error
2616 }{
2617 {
2618 name: "basic word match",
2619 options: GrepOptions{
2620 Patterns: []*regexp.Regexp{regexp.MustCompile("import")},
2621 },
2622 wantResult: []GrepResult{
2623 {
2624 FileName: "go/example.go",
2625 LineNumber: 3,
2626 Content: "import (",
2627 TreeName: "6ecf0ef2c2dffb796033e5a02219af86ec6584e5",
2628 },
2629 {
2630 FileName: "vendor/foo.go",
2631 LineNumber: 3,
2632 Content: "import \"fmt\"",
2633 TreeName: "6ecf0ef2c2dffb796033e5a02219af86ec6584e5",
2634 },
2635 },
2636 }, {
2637 name: "case insensitive match",
2638 options: GrepOptions{
2639 Patterns: []*regexp.Regexp{regexp.MustCompile(`(?i)IMport`)},
2640 },
2641 wantResult: []GrepResult{
2642 {
2643 FileName: "go/example.go",
2644 LineNumber: 3,
2645 Content: "import (",
2646 TreeName: "6ecf0ef2c2dffb796033e5a02219af86ec6584e5",
2647 },
2648 {
2649 FileName: "vendor/foo.go",
2650 LineNumber: 3,
2651 Content: "import \"fmt\"",
2652 TreeName: "6ecf0ef2c2dffb796033e5a02219af86ec6584e5",
2653 },
2654 },
2655 }, {
2656 name: "invert match",
2657 options: GrepOptions{
2658 Patterns: []*regexp.Regexp{regexp.MustCompile("import")},
2659 InvertMatch: true,
2660 },
2661 dontWantResult: []GrepResult{
2662 {
2663 FileName: "go/example.go",
2664 LineNumber: 3,
2665 Content: "import (",
2666 TreeName: "6ecf0ef2c2dffb796033e5a02219af86ec6584e5",
2667 },
2668 {
2669 FileName: "vendor/foo.go",
2670 LineNumber: 3,
2671 Content: "import \"fmt\"",
2672 TreeName: "6ecf0ef2c2dffb796033e5a02219af86ec6584e5",
2673 },
2674 },
2675 }, {
2676 name: "match at a given commit hash",
2677 options: GrepOptions{
2678 Patterns: []*regexp.Regexp{regexp.MustCompile("The MIT License")},
2679 CommitHash: plumbing.NewHash("b029517f6300c2da0f4b651b8642506cd6aaf45d"),
2680 },
2681 wantResult: []GrepResult{
2682 {
2683 FileName: "LICENSE",
2684 LineNumber: 1,
2685 Content: "The MIT License (MIT)",
2686 TreeName: "b029517f6300c2da0f4b651b8642506cd6aaf45d",
2687 },
2688 },
2689 dontWantResult: []GrepResult{
2690 {
2691 FileName: "go/example.go",
2692 LineNumber: 3,
2693 Content: "import (",
2694 TreeName: "6ecf0ef2c2dffb796033e5a02219af86ec6584e5",
2695 },
2696 },
2697 }, {
2698 name: "match for a given pathspec",
2699 options: GrepOptions{
2700 Patterns: []*regexp.Regexp{regexp.MustCompile("import")},
2701 PathSpecs: []*regexp.Regexp{regexp.MustCompile("go/")},
2702 },
2703 wantResult: []GrepResult{
2704 {
2705 FileName: "go/example.go",
2706 LineNumber: 3,
2707 Content: "import (",
2708 TreeName: "6ecf0ef2c2dffb796033e5a02219af86ec6584e5",
2709 },
2710 },
2711 dontWantResult: []GrepResult{
2712 {
2713 FileName: "vendor/foo.go",
2714 LineNumber: 3,
2715 Content: "import \"fmt\"",
2716 TreeName: "6ecf0ef2c2dffb796033e5a02219af86ec6584e5",
2717 },
2718 },
2719 }, {
2720 name: "match at a given reference name",
2721 options: GrepOptions{
2722 Patterns: []*regexp.Regexp{regexp.MustCompile("import")},
2723 ReferenceName: "refs/heads/master",
2724 },
2725 wantResult: []GrepResult{
2726 {
2727 FileName: "go/example.go",
2728 LineNumber: 3,
2729 Content: "import (",
2730 TreeName: "refs/heads/master",
2731 },
2732 },
2733 }, {
2734 name: "ambiguous options",
2735 options: GrepOptions{
2736 Patterns: []*regexp.Regexp{regexp.MustCompile("import")},
2737 CommitHash: plumbing.NewHash("2d55a722f3c3ecc36da919dfd8b6de38352f3507"),
2738 ReferenceName: "somereferencename",
2739 },
2740 wantError: ErrHashOrReference,
2741 }, {
2742 name: "multiple patterns",
2743 options: GrepOptions{
2744 Patterns: []*regexp.Regexp{
2745 regexp.MustCompile("import"),
2746 regexp.MustCompile("License"),
2747 },
2748 },
2749 wantResult: []GrepResult{
2750 {
2751 FileName: "go/example.go",
2752 LineNumber: 3,
2753 Content: "import (",
2754 TreeName: "6ecf0ef2c2dffb796033e5a02219af86ec6584e5",
2755 },
2756 {
2757 FileName: "vendor/foo.go",
2758 LineNumber: 3,
2759 Content: "import \"fmt\"",
2760 TreeName: "6ecf0ef2c2dffb796033e5a02219af86ec6584e5",
2761 },
2762 {
2763 FileName: "LICENSE",
2764 LineNumber: 1,
2765 Content: "The MIT License (MIT)",
2766 TreeName: "6ecf0ef2c2dffb796033e5a02219af86ec6584e5",
2767 },
2768 },
2769 }, {
2770 name: "multiple pathspecs",
2771 options: GrepOptions{
2772 Patterns: []*regexp.Regexp{regexp.MustCompile("import")},
2773 PathSpecs: []*regexp.Regexp{
2774 regexp.MustCompile("go/"),
2775 regexp.MustCompile("vendor/"),
2776 },
2777 },
2778 wantResult: []GrepResult{
2779 {
2780 FileName: "go/example.go",
2781 LineNumber: 3,
2782 Content: "import (",
2783 TreeName: "6ecf0ef2c2dffb796033e5a02219af86ec6584e5",
2784 },
2785 {
2786 FileName: "vendor/foo.go",
2787 LineNumber: 3,
2788 Content: "import \"fmt\"",
2789 TreeName: "6ecf0ef2c2dffb796033e5a02219af86ec6584e5",
2790 },
2791 },
2792 },
2793 }
2794
2795 path := fixtures.Basic().ByTag("worktree").One().Worktree().Root()
2796
2797 dir, err := os.MkdirTemp("", "")
2798 s.NoError(err)
2799
2800 server, err := PlainClone(dir, false, &CloneOptions{
2801 URL: path,
2802 })
2803 s.NoError(err)
2804
2805 w, err := server.Worktree()
2806 s.NoError(err)
2807
2808 for _, tc := range cases {
2809 gr, err := w.Grep(&tc.options)
2810 if tc.wantError != nil {
2811 s.ErrorIs(err, tc.wantError)
2812 } else {
2813 s.NoError(err)
2814 }
2815
2816 // Iterate through the results and check if the wanted result is present
2817 // in the got result.
2818 for _, wantResult := range tc.wantResult {
2819 found := false
2820 for _, gotResult := range gr {
2821 if wantResult == gotResult {
2822 found = true
2823 break
2824 }
2825 }
2826 if !found {
2827 s.T().Errorf("unexpected grep results for %q, expected result to contain: %v", tc.name, wantResult)
2828 }
2829 }
2830
2831 // Iterate through the results and check if the not wanted result is
2832 // present in the got result.
2833 for _, dontWantResult := range tc.dontWantResult {
2834 found := false
2835 for _, gotResult := range gr {
2836 if dontWantResult == gotResult {
2837 found = true
2838 break
2839 }
2840 }
2841 if found {
2842 s.T().Errorf("unexpected grep results for %q, expected result to NOT contain: %v", tc.name, dontWantResult)
2843 }
2844 }
2845 }
2846}
2847
2848func (s *WorktreeSuite) TestGrepBare() {
2849 cases := []struct {
2850 name string
2851 options GrepOptions
2852 wantResult []GrepResult
2853 dontWantResult []GrepResult
2854 wantError error
2855 }{
2856 {
2857 name: "basic word match",
2858 options: GrepOptions{
2859 Patterns: []*regexp.Regexp{regexp.MustCompile("import")},
2860 CommitHash: plumbing.ZeroHash,
2861 },
2862 wantResult: []GrepResult{
2863 {
2864 FileName: "go/example.go",
2865 LineNumber: 3,
2866 Content: "import (",
2867 TreeName: "6ecf0ef2c2dffb796033e5a02219af86ec6584e5",
2868 },
2869 {
2870 FileName: "vendor/foo.go",
2871 LineNumber: 3,
2872 Content: "import \"fmt\"",
2873 TreeName: "6ecf0ef2c2dffb796033e5a02219af86ec6584e5",
2874 },
2875 },
2876 },
2877 }
2878
2879 path := fixtures.Basic().ByTag("worktree").One().Worktree().Root()
2880
2881 dir, err := os.MkdirTemp("", "")
2882 s.NoError(err)
2883
2884 r, err := PlainClone(dir, true, &CloneOptions{
2885 URL: path,
2886 })
2887 s.NoError(err)
2888
2889 for _, tc := range cases {
2890 gr, err := r.Grep(&tc.options)
2891 if tc.wantError != nil {
2892 s.ErrorIs(err, tc.wantError)
2893 } else {
2894 s.NoError(err)
2895 }
2896
2897 // Iterate through the results and check if the wanted result is present
2898 // in the got result.
2899 for _, wantResult := range tc.wantResult {
2900 found := false
2901 for _, gotResult := range gr {
2902 if wantResult == gotResult {
2903 found = true
2904 break
2905 }
2906 }
2907 if !found {
2908 s.T().Errorf("unexpected grep results for %q, expected result to contain: %v", tc.name, wantResult)
2909 }
2910 }
2911
2912 // Iterate through the results and check if the not wanted result is
2913 // present in the got result.
2914 for _, dontWantResult := range tc.dontWantResult {
2915 found := false
2916 for _, gotResult := range gr {
2917 if dontWantResult == gotResult {
2918 found = true
2919 break
2920 }
2921 }
2922 if found {
2923 s.T().Errorf("unexpected grep results for %q, expected result to NOT contain: %v", tc.name, dontWantResult)
2924 }
2925 }
2926 }
2927}
2928
2929func (s *WorktreeSuite) TestResetLingeringDirectories() {
2930 dir, err := os.MkdirTemp("", "")
2931 s.NoError(err)
2932
2933 commitOpts := &CommitOptions{Author: &object.Signature{
2934 Name: "foo",
2935 Email: "foo@foo.foo",
2936 When: time.Now(),
2937 }}
2938
2939 repo, err := PlainInit(dir, false)
2940 s.NoError(err)
2941
2942 w, err := repo.Worktree()
2943 s.NoError(err)
2944
2945 os.WriteFile(filepath.Join(dir, "README"), []byte("placeholder"), 0o644)
2946
2947 _, err = w.Add(".")
2948 s.NoError(err)
2949
2950 initialHash, err := w.Commit("Initial commit", commitOpts)
2951 s.NoError(err)
2952
2953 os.MkdirAll(filepath.Join(dir, "a", "b"), 0o755)
2954 os.WriteFile(filepath.Join(dir, "a", "b", "1"), []byte("1"), 0o644)
2955
2956 _, err = w.Add(".")
2957 s.NoError(err)
2958
2959 _, err = w.Commit("Add file in nested sub-directories", commitOpts)
2960 s.NoError(err)
2961
2962 // reset to initial commit, which should remove a/b/1, a/b, and a
2963 err = w.Reset(&ResetOptions{
2964 Commit: initialHash,
2965 Mode: HardReset,
2966 })
2967 s.NoError(err)
2968
2969 _, err = os.Stat(filepath.Join(dir, "a", "b", "1"))
2970 s.True(errors.Is(err, os.ErrNotExist))
2971
2972 _, err = os.Stat(filepath.Join(dir, "a", "b"))
2973 s.True(errors.Is(err, os.ErrNotExist))
2974
2975 _, err = os.Stat(filepath.Join(dir, "a"))
2976 s.True(errors.Is(err, os.ErrNotExist))
2977}
2978
2979func (s *WorktreeSuite) TestAddAndCommit() {
2980 expectedFiles := 2
2981
2982 dir, err := os.MkdirTemp("", "")
2983 s.NoError(err)
2984
2985 repo, err := PlainInit(dir, false)
2986 s.NoError(err)
2987
2988 w, err := repo.Worktree()
2989 s.NoError(err)
2990
2991 os.WriteFile(filepath.Join(dir, "foo"), []byte("bar"), 0o644)
2992 os.WriteFile(filepath.Join(dir, "bar"), []byte("foo"), 0o644)
2993
2994 _, err = w.Add(".")
2995 s.NoError(err)
2996
2997 _, err = w.Commit("Test Add And Commit", &CommitOptions{Author: &object.Signature{
2998 Name: "foo",
2999 Email: "foo@foo.foo",
3000 When: time.Now(),
3001 }})
3002 s.NoError(err)
3003
3004 iter, err := w.r.Log(&LogOptions{})
3005 s.NoError(err)
3006
3007 filesFound := 0
3008 err = iter.ForEach(func(c *object.Commit) error {
3009 files, err := c.Files()
3010 if err != nil {
3011 return err
3012 }
3013
3014 err = files.ForEach(func(f *object.File) error {
3015 filesFound++
3016 return nil
3017 })
3018 return err
3019 })
3020 s.NoError(err)
3021 s.Equal(expectedFiles, filesFound)
3022}
3023
3024func (s *WorktreeSuite) TestAddAndCommitEmpty() {
3025 dir, err := os.MkdirTemp("", "")
3026 s.NoError(err)
3027
3028 repo, err := PlainInit(dir, false)
3029 s.NoError(err)
3030
3031 w, err := repo.Worktree()
3032 s.NoError(err)
3033
3034 _, err = w.Add(".")
3035 s.NoError(err)
3036
3037 _, err = w.Commit("Test Add And Commit", &CommitOptions{Author: &object.Signature{
3038 Name: "foo",
3039 Email: "foo@foo.foo",
3040 When: time.Now(),
3041 }})
3042 s.ErrorIs(err, ErrEmptyCommit)
3043}
3044
3045func (s *WorktreeSuite) TestLinkedWorktree() {
3046 fs := fixtures.ByTag("linked-worktree").One().Worktree()
3047
3048 // Open main repo.
3049 {
3050 fs, err := fs.Chroot("main")
3051 s.NoError(err)
3052 repo, err := PlainOpenWithOptions(fs.Root(), &PlainOpenOptions{EnableDotGitCommonDir: true})
3053 s.NoError(err)
3054
3055 wt, err := repo.Worktree()
3056 s.NoError(err)
3057
3058 status, err := wt.Status()
3059 s.NoError(err)
3060 s.Len(status, 2) // 2 files
3061
3062 head, err := repo.Head()
3063 s.NoError(err)
3064 s.Equal("refs/heads/master", string(head.Name()))
3065 }
3066
3067 // Open linked-worktree #1.
3068 {
3069 fs, err := fs.Chroot("linked-worktree-1")
3070 s.NoError(err)
3071 repo, err := PlainOpenWithOptions(fs.Root(), &PlainOpenOptions{EnableDotGitCommonDir: true})
3072 s.NoError(err)
3073
3074 wt, err := repo.Worktree()
3075 s.NoError(err)
3076
3077 status, err := wt.Status()
3078 s.NoError(err)
3079 s.Len(status, 3) // 3 files
3080
3081 _, ok := status["linked-worktree-1-unique-file.txt"]
3082 s.True(ok)
3083
3084 head, err := repo.Head()
3085 s.NoError(err)
3086 s.Equal("refs/heads/linked-worktree-1", string(head.Name()))
3087 }
3088
3089 // Open linked-worktree #2.
3090 {
3091 fs, err := fs.Chroot("linked-worktree-2")
3092 s.NoError(err)
3093 repo, err := PlainOpenWithOptions(fs.Root(), &PlainOpenOptions{EnableDotGitCommonDir: true})
3094 s.NoError(err)
3095
3096 wt, err := repo.Worktree()
3097 s.NoError(err)
3098
3099 status, err := wt.Status()
3100 s.NoError(err)
3101 s.Len(status, 3) // 3 files
3102
3103 _, ok := status["linked-worktree-2-unique-file.txt"]
3104 s.True(ok)
3105
3106 head, err := repo.Head()
3107 s.NoError(err)
3108 s.Equal("refs/heads/branch-with-different-name", string(head.Name()))
3109 }
3110
3111 // Open linked-worktree #2.
3112 {
3113 fs, err := fs.Chroot("linked-worktree-invalid-commondir")
3114 s.NoError(err)
3115 _, err = PlainOpenWithOptions(fs.Root(), &PlainOpenOptions{EnableDotGitCommonDir: true})
3116 s.ErrorIs(err, ErrRepositoryIncomplete)
3117 }
3118}
3119
3120func TestValidPath(t *testing.T) {
3121 type testcase struct {
3122 path string
3123 wantErr bool
3124 }
3125
3126 tests := []testcase{
3127 {".git", true},
3128 {".git/b", true},
3129 {".git\\b", true},
3130 {"git~1", true},
3131 {"a/../b", true},
3132 {"a\\..\\b", true},
3133 {"/", true},
3134 {"", true},
3135 {".gitmodules", false},
3136 {".gitignore", false},
3137 {"a..b", false},
3138 {".", false},
3139 {"a/.git", false},
3140 {"a\\.git", false},
3141 {"a/.git/b", false},
3142 {"a\\.git\\b", false},
3143 }
3144
3145 if runtime.GOOS == "windows" {
3146 tests = append(tests, []testcase{
3147 {"\\\\a\\b", true},
3148 {"C:\\a\\b", true},
3149 {".git . . .", true},
3150 {".git . . ", true},
3151 {".git ", true},
3152 {".git.", true},
3153 {".git::$INDEX_ALLOCATION", true},
3154 }...)
3155 }
3156
3157 for _, tc := range tests {
3158 t.Run(tc.path, func(t *testing.T) {
3159 err := validPath(tc.path)
3160 if tc.wantErr {
3161 assert.Error(t, err)
3162 } else {
3163 assert.NoError(t, err)
3164 }
3165 })
3166 }
3167}
3168
3169func TestWindowsValidPath(t *testing.T) {
3170 tests := []struct {
3171 path string
3172 want bool
3173 }{
3174 {".git", false},
3175 {".git . . .", false},
3176 {".git ", false},
3177 {".git ", false},
3178 {".git . .", false},
3179 {".git . .", false},
3180 {".git::$INDEX_ALLOCATION", false},
3181 {".git:", false},
3182 {"a", true},
3183 {"a\\b", true},
3184 {"a/b", true},
3185 {".gitm", true},
3186 }
3187
3188 for _, tc := range tests {
3189 t.Run(tc.path, func(t *testing.T) {
3190 got := windowsValidPath(tc.path)
3191 assert.Equal(t, tc.want, got)
3192 })
3193 }
3194}
3195
3196var statusCodeNames = map[StatusCode]string{
3197 Unmodified: "Unmodified",
3198 Untracked: "Untracked",
3199 Modified: "Modified",
3200 Added: "Added",
3201 Deleted: "Deleted",
3202 Renamed: "Renamed",
3203 Copied: "Copied",
3204 UpdatedButUnmerged: "UpdatedButUnmerged",
3205}
3206
3207func setupForRestore(s *WorktreeSuite) (fs billy.Filesystem, w *Worktree, names []string) {
3208 fs = memfs.New()
3209 w = &Worktree{
3210 r: s.Repository,
3211 Filesystem: fs,
3212 }
3213
3214 err := w.Checkout(&CheckoutOptions{})
3215 s.NoError(err)
3216
3217 names = []string{"foo", "CHANGELOG", "LICENSE", "binary.jpg"}
3218 verifyStatus(s, "Checkout", w, names, []FileStatus{
3219 {Worktree: Untracked, Staging: Untracked},
3220 {Worktree: Untracked, Staging: Untracked},
3221 {Worktree: Untracked, Staging: Untracked},
3222 {Worktree: Untracked, Staging: Untracked},
3223 })
3224
3225 // Touch of bunch of files including create a new file and delete an exsiting file
3226 for _, name := range names {
3227 err = util.WriteFile(fs, name, []byte("Foo Bar"), 0o755)
3228 s.NoError(err)
3229 }
3230 err = util.RemoveAll(fs, names[3])
3231 s.NoError(err)
3232
3233 // Confirm the status after doing the edits without staging anything
3234 verifyStatus(s, "Edits", w, names, []FileStatus{
3235 {Worktree: Untracked, Staging: Untracked},
3236 {Worktree: Modified, Staging: Unmodified},
3237 {Worktree: Modified, Staging: Unmodified},
3238 {Worktree: Deleted, Staging: Unmodified},
3239 })
3240
3241 // Stage all files and verify the updated status
3242 for _, name := range names {
3243 _, err = w.Add(name)
3244 s.NoError(err)
3245 }
3246 verifyStatus(s, "Staged", w, names, []FileStatus{
3247 {Worktree: Unmodified, Staging: Added},
3248 {Worktree: Unmodified, Staging: Modified},
3249 {Worktree: Unmodified, Staging: Modified},
3250 {Worktree: Unmodified, Staging: Deleted},
3251 })
3252
3253 // Add secondary changes to a file to make sure we only restore the staged file
3254 err = util.WriteFile(fs, names[1], []byte("Foo Bar:11"), 0755)
3255 s.NoError(err)
3256 err = util.WriteFile(fs, names[2], []byte("Foo Bar:22"), 0755)
3257 s.NoError(err)
3258
3259 verifyStatus(s, "Secondary Edits", w, names, []FileStatus{
3260 {Worktree: Unmodified, Staging: Added},
3261 {Worktree: Modified, Staging: Modified},
3262 {Worktree: Modified, Staging: Modified},
3263 {Worktree: Unmodified, Staging: Deleted},
3264 })
3265
3266 return
3267}
3268
3269func verifyStatus(s *WorktreeSuite, marker string, w *Worktree, files []string, statuses []FileStatus) {
3270 s.Len(statuses, len(files))
3271
3272 status, err := w.Status()
3273 s.NoError(err)
3274
3275 for i, file := range files {
3276 current := status.File(file)
3277 expected := statuses[i]
3278 s.Equal(expected.Worktree, current.Worktree, fmt.Sprintf("%s - [%d] : %s Worktree %s != %s", marker, i, file, statusCodeNames[current.Worktree], statusCodeNames[expected.Worktree]))
3279 s.Equal(expected.Staging, current.Staging, fmt.Sprintf("%s - [%d] : %s Staging %s != %s", marker, i, file, statusCodeNames[current.Staging], statusCodeNames[expected.Staging]))
3280 }
3281}
3282
3283func (s *WorktreeSuite) TestRestoreStaged() {
3284 fs, w, names := setupForRestore(s)
3285
3286 // Attempt without files should throw an error like the git restore --staged
3287 opts := RestoreOptions{Staged: true}
3288 err := w.Restore(&opts)
3289 s.ErrorIs(err, ErrNoRestorePaths)
3290
3291 // Restore Staged files in 2 groups and confirm status
3292 opts.Files = []string{names[0], "./" + names[1]}
3293 err = w.Restore(&opts)
3294 s.NoError(err)
3295 verifyStatus(s, "Restored First", w, names, []FileStatus{
3296 {Worktree: Untracked, Staging: Untracked},
3297 {Worktree: Modified, Staging: Unmodified},
3298 {Worktree: Modified, Staging: Modified},
3299 {Worktree: Unmodified, Staging: Deleted},
3300 })
3301
3302 // Make sure the restore didn't overwrite our secondary changes
3303 contents, err := util.ReadFile(fs, names[1])
3304 s.NoError(err)
3305 s.Equal("Foo Bar:11", string(contents))
3306
3307 opts.Files = []string{"./" + names[2], names[3]}
3308 err = w.Restore(&opts)
3309 s.NoError(err)
3310 verifyStatus(s, "Restored Second", w, names, []FileStatus{
3311 {Worktree: Untracked, Staging: Untracked},
3312 {Worktree: Modified, Staging: Unmodified},
3313 {Worktree: Modified, Staging: Unmodified},
3314 {Worktree: Deleted, Staging: Unmodified},
3315 })
3316
3317 // Make sure the restore didn't overwrite our secondary changes
3318 contents, err = util.ReadFile(fs, names[2])
3319 s.NoError(err)
3320 s.Equal("Foo Bar:22", string(contents))
3321}
3322
3323func (s *WorktreeSuite) TestRestoreWorktree() {
3324 _, w, names := setupForRestore(s)
3325
3326 // Attempt without files should throw an error like the git restore
3327 opts := RestoreOptions{}
3328 err := w.Restore(&opts)
3329 s.ErrorIs(err, ErrNoRestorePaths)
3330
3331 opts.Files = []string{names[0], names[1]}
3332 err = w.Restore(&opts)
3333 s.ErrorIs(err, ErrRestoreWorktreeOnlyNotSupported)
3334}
3335
3336func (s *WorktreeSuite) TestRestoreBoth() {
3337 _, w, names := setupForRestore(s)
3338
3339 // Attempt without files should throw an error like the git restore --staged --worktree
3340 opts := RestoreOptions{Staged: true, Worktree: true}
3341 err := w.Restore(&opts)
3342 s.ErrorIs(err, ErrNoRestorePaths)
3343
3344 // Restore Staged files in 2 groups and confirm status
3345 opts.Files = []string{names[0], names[1]}
3346 err = w.Restore(&opts)
3347 s.NoError(err)
3348 verifyStatus(s, "Restored First", w, names, []FileStatus{
3349 {Worktree: Untracked, Staging: Untracked},
3350 {Worktree: Untracked, Staging: Untracked},
3351 {Worktree: Modified, Staging: Modified},
3352 {Worktree: Unmodified, Staging: Deleted},
3353 })
3354
3355 opts.Files = []string{names[2], names[3]}
3356 err = w.Restore(&opts)
3357 s.NoError(err)
3358 verifyStatus(s, "Restored Second", w, names, []FileStatus{
3359 {Worktree: Untracked, Staging: Untracked},
3360 {Worktree: Untracked, Staging: Untracked},
3361 {Worktree: Untracked, Staging: Untracked},
3362 {Worktree: Untracked, Staging: Untracked},
3363 })
3364}