fork of go-git with some jj specific features
at main 77 kB view raw
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}