fork of go-git with some jj specific features
at v5.8.0 22 kB view raw
1package git 2 3import ( 4 "context" 5 "errors" 6 "fmt" 7 "io" 8 "os" 9 "path/filepath" 10 "strings" 11 12 "github.com/go-git/go-billy/v5" 13 "github.com/go-git/go-billy/v5/util" 14 "github.com/go-git/go-git/v5/config" 15 "github.com/go-git/go-git/v5/plumbing" 16 "github.com/go-git/go-git/v5/plumbing/filemode" 17 "github.com/go-git/go-git/v5/plumbing/format/gitignore" 18 "github.com/go-git/go-git/v5/plumbing/format/index" 19 "github.com/go-git/go-git/v5/plumbing/object" 20 "github.com/go-git/go-git/v5/plumbing/storer" 21 "github.com/go-git/go-git/v5/utils/ioutil" 22 "github.com/go-git/go-git/v5/utils/merkletrie" 23 "github.com/go-git/go-git/v5/utils/sync" 24) 25 26var ( 27 ErrWorktreeNotClean = errors.New("worktree is not clean") 28 ErrSubmoduleNotFound = errors.New("submodule not found") 29 ErrUnstagedChanges = errors.New("worktree contains unstaged changes") 30 ErrGitModulesSymlink = errors.New(gitmodulesFile + " is a symlink") 31 ErrNonFastForwardUpdate = errors.New("non-fast-forward update") 32) 33 34// Worktree represents a git worktree. 35type Worktree struct { 36 // Filesystem underlying filesystem. 37 Filesystem billy.Filesystem 38 // External excludes not found in the repository .gitignore 39 Excludes []gitignore.Pattern 40 41 r *Repository 42} 43 44// Pull incorporates changes from a remote repository into the current branch. 45// Returns nil if the operation is successful, NoErrAlreadyUpToDate if there are 46// no changes to be fetched, or an error. 47// 48// Pull only supports merges where the can be resolved as a fast-forward. 49func (w *Worktree) Pull(o *PullOptions) error { 50 return w.PullContext(context.Background(), o) 51} 52 53// PullContext incorporates changes from a remote repository into the current 54// branch. Returns nil if the operation is successful, NoErrAlreadyUpToDate if 55// there are no changes to be fetched, or an error. 56// 57// Pull only supports merges where the can be resolved as a fast-forward. 58// 59// The provided Context must be non-nil. If the context expires before the 60// operation is complete, an error is returned. The context only affects the 61// transport operations. 62func (w *Worktree) PullContext(ctx context.Context, o *PullOptions) error { 63 if err := o.Validate(); err != nil { 64 return err 65 } 66 67 remote, err := w.r.Remote(o.RemoteName) 68 if err != nil { 69 return err 70 } 71 72 fetchHead, err := remote.fetch(ctx, &FetchOptions{ 73 RemoteName: o.RemoteName, 74 RemoteURL: o.RemoteURL, 75 Depth: o.Depth, 76 Auth: o.Auth, 77 Progress: o.Progress, 78 Force: o.Force, 79 InsecureSkipTLS: o.InsecureSkipTLS, 80 CABundle: o.CABundle, 81 }) 82 83 updated := true 84 if err == NoErrAlreadyUpToDate { 85 updated = false 86 } else if err != nil { 87 return err 88 } 89 90 ref, err := storer.ResolveReference(fetchHead, o.ReferenceName) 91 if err != nil { 92 return err 93 } 94 95 head, err := w.r.Head() 96 if err == nil { 97 headAheadOfRef, err := isFastForward(w.r.Storer, ref.Hash(), head.Hash()) 98 if err != nil { 99 return err 100 } 101 102 if !updated && headAheadOfRef { 103 return NoErrAlreadyUpToDate 104 } 105 106 ff, err := isFastForward(w.r.Storer, head.Hash(), ref.Hash()) 107 if err != nil { 108 return err 109 } 110 111 if !ff { 112 return ErrNonFastForwardUpdate 113 } 114 } 115 116 if err != nil && err != plumbing.ErrReferenceNotFound { 117 return err 118 } 119 120 if err := w.updateHEAD(ref.Hash()); err != nil { 121 return err 122 } 123 124 if err := w.Reset(&ResetOptions{ 125 Mode: MergeReset, 126 Commit: ref.Hash(), 127 }); err != nil { 128 return err 129 } 130 131 if o.RecurseSubmodules != NoRecurseSubmodules { 132 return w.updateSubmodules(&SubmoduleUpdateOptions{ 133 RecurseSubmodules: o.RecurseSubmodules, 134 Auth: o.Auth, 135 }) 136 } 137 138 return nil 139} 140 141func (w *Worktree) updateSubmodules(o *SubmoduleUpdateOptions) error { 142 s, err := w.Submodules() 143 if err != nil { 144 return err 145 } 146 o.Init = true 147 return s.Update(o) 148} 149 150// Checkout switch branches or restore working tree files. 151func (w *Worktree) Checkout(opts *CheckoutOptions) error { 152 if err := opts.Validate(); err != nil { 153 return err 154 } 155 156 if opts.Create { 157 if err := w.createBranch(opts); err != nil { 158 return err 159 } 160 } 161 162 c, err := w.getCommitFromCheckoutOptions(opts) 163 if err != nil { 164 return err 165 } 166 167 ro := &ResetOptions{Commit: c, Mode: MergeReset} 168 if opts.Force { 169 ro.Mode = HardReset 170 } else if opts.Keep { 171 ro.Mode = SoftReset 172 } 173 174 if !opts.Hash.IsZero() && !opts.Create { 175 err = w.setHEADToCommit(opts.Hash) 176 } else { 177 err = w.setHEADToBranch(opts.Branch, c) 178 } 179 180 if err != nil { 181 return err 182 } 183 184 if len(opts.SparseCheckoutDirectories) > 0 { 185 return w.ResetSparsely(ro, opts.SparseCheckoutDirectories) 186 } 187 188 return w.Reset(ro) 189} 190func (w *Worktree) createBranch(opts *CheckoutOptions) error { 191 _, err := w.r.Storer.Reference(opts.Branch) 192 if err == nil { 193 return fmt.Errorf("a branch named %q already exists", opts.Branch) 194 } 195 196 if err != plumbing.ErrReferenceNotFound { 197 return err 198 } 199 200 if opts.Hash.IsZero() { 201 ref, err := w.r.Head() 202 if err != nil { 203 return err 204 } 205 206 opts.Hash = ref.Hash() 207 } 208 209 return w.r.Storer.SetReference( 210 plumbing.NewHashReference(opts.Branch, opts.Hash), 211 ) 212} 213 214func (w *Worktree) getCommitFromCheckoutOptions(opts *CheckoutOptions) (plumbing.Hash, error) { 215 if !opts.Hash.IsZero() { 216 return opts.Hash, nil 217 } 218 219 b, err := w.r.Reference(opts.Branch, true) 220 if err != nil { 221 return plumbing.ZeroHash, err 222 } 223 224 if !b.Name().IsTag() { 225 return b.Hash(), nil 226 } 227 228 o, err := w.r.Object(plumbing.AnyObject, b.Hash()) 229 if err != nil { 230 return plumbing.ZeroHash, err 231 } 232 233 switch o := o.(type) { 234 case *object.Tag: 235 if o.TargetType != plumbing.CommitObject { 236 return plumbing.ZeroHash, fmt.Errorf("unsupported tag object target %q", o.TargetType) 237 } 238 239 return o.Target, nil 240 case *object.Commit: 241 return o.Hash, nil 242 } 243 244 return plumbing.ZeroHash, fmt.Errorf("unsupported tag target %q", o.Type()) 245} 246 247func (w *Worktree) setHEADToCommit(commit plumbing.Hash) error { 248 head := plumbing.NewHashReference(plumbing.HEAD, commit) 249 return w.r.Storer.SetReference(head) 250} 251 252func (w *Worktree) setHEADToBranch(branch plumbing.ReferenceName, commit plumbing.Hash) error { 253 target, err := w.r.Storer.Reference(branch) 254 if err != nil { 255 return err 256 } 257 258 var head *plumbing.Reference 259 if target.Name().IsBranch() { 260 head = plumbing.NewSymbolicReference(plumbing.HEAD, target.Name()) 261 } else { 262 head = plumbing.NewHashReference(plumbing.HEAD, commit) 263 } 264 265 return w.r.Storer.SetReference(head) 266} 267 268func (w *Worktree) ResetSparsely(opts *ResetOptions, dirs []string) error { 269 if err := opts.Validate(w.r); err != nil { 270 return err 271 } 272 273 if opts.Mode == MergeReset { 274 unstaged, err := w.containsUnstagedChanges() 275 if err != nil { 276 return err 277 } 278 279 if unstaged { 280 return ErrUnstagedChanges 281 } 282 } 283 284 if err := w.setHEADCommit(opts.Commit); err != nil { 285 return err 286 } 287 288 if opts.Mode == SoftReset { 289 return nil 290 } 291 292 t, err := w.r.getTreeFromCommitHash(opts.Commit) 293 if err != nil { 294 return err 295 } 296 297 if opts.Mode == MixedReset || opts.Mode == MergeReset || opts.Mode == HardReset { 298 if err := w.resetIndex(t, dirs); err != nil { 299 return err 300 } 301 } 302 303 if opts.Mode == MergeReset || opts.Mode == HardReset { 304 if err := w.resetWorktree(t); err != nil { 305 return err 306 } 307 } 308 309 return nil 310} 311 312// Reset the worktree to a specified state. 313func (w *Worktree) Reset(opts *ResetOptions) error { 314 return w.ResetSparsely(opts, nil) 315} 316 317func (w *Worktree) resetIndex(t *object.Tree, dirs []string) error { 318 idx, err := w.r.Storer.Index() 319 if len(dirs) > 0 { 320 idx.SkipUnless(dirs) 321 } 322 323 if err != nil { 324 return err 325 } 326 b := newIndexBuilder(idx) 327 328 changes, err := w.diffTreeWithStaging(t, true) 329 if err != nil { 330 return err 331 } 332 333 for _, ch := range changes { 334 a, err := ch.Action() 335 if err != nil { 336 return err 337 } 338 339 var name string 340 var e *object.TreeEntry 341 342 switch a { 343 case merkletrie.Modify, merkletrie.Insert: 344 name = ch.To.String() 345 e, err = t.FindEntry(name) 346 if err != nil { 347 return err 348 } 349 case merkletrie.Delete: 350 name = ch.From.String() 351 } 352 353 b.Remove(name) 354 if e == nil { 355 continue 356 } 357 358 b.Add(&index.Entry{ 359 Name: name, 360 Hash: e.Hash, 361 Mode: e.Mode, 362 }) 363 364 } 365 366 b.Write(idx) 367 return w.r.Storer.SetIndex(idx) 368} 369 370func (w *Worktree) resetWorktree(t *object.Tree) error { 371 changes, err := w.diffStagingWithWorktree(true) 372 if err != nil { 373 return err 374 } 375 376 idx, err := w.r.Storer.Index() 377 if err != nil { 378 return err 379 } 380 b := newIndexBuilder(idx) 381 382 for _, ch := range changes { 383 if err := w.checkoutChange(ch, t, b); err != nil { 384 return err 385 } 386 } 387 388 b.Write(idx) 389 return w.r.Storer.SetIndex(idx) 390} 391 392func (w *Worktree) checkoutChange(ch merkletrie.Change, t *object.Tree, idx *indexBuilder) error { 393 a, err := ch.Action() 394 if err != nil { 395 return err 396 } 397 398 var e *object.TreeEntry 399 var name string 400 var isSubmodule bool 401 402 switch a { 403 case merkletrie.Modify, merkletrie.Insert: 404 name = ch.To.String() 405 e, err = t.FindEntry(name) 406 if err != nil { 407 return err 408 } 409 410 isSubmodule = e.Mode == filemode.Submodule 411 case merkletrie.Delete: 412 return rmFileAndDirsIfEmpty(w.Filesystem, ch.From.String()) 413 } 414 415 if isSubmodule { 416 return w.checkoutChangeSubmodule(name, a, e, idx) 417 } 418 419 return w.checkoutChangeRegularFile(name, a, t, e, idx) 420} 421 422func (w *Worktree) containsUnstagedChanges() (bool, error) { 423 ch, err := w.diffStagingWithWorktree(false) 424 if err != nil { 425 return false, err 426 } 427 428 for _, c := range ch { 429 a, err := c.Action() 430 if err != nil { 431 return false, err 432 } 433 434 if a == merkletrie.Insert { 435 continue 436 } 437 438 return true, nil 439 } 440 441 return false, nil 442} 443 444func (w *Worktree) setHEADCommit(commit plumbing.Hash) error { 445 head, err := w.r.Reference(plumbing.HEAD, false) 446 if err != nil { 447 return err 448 } 449 450 if head.Type() == plumbing.HashReference { 451 head = plumbing.NewHashReference(plumbing.HEAD, commit) 452 return w.r.Storer.SetReference(head) 453 } 454 455 branch, err := w.r.Reference(head.Target(), false) 456 if err != nil { 457 return err 458 } 459 460 if !branch.Name().IsBranch() { 461 return fmt.Errorf("invalid HEAD target should be a branch, found %s", branch.Type()) 462 } 463 464 branch = plumbing.NewHashReference(branch.Name(), commit) 465 return w.r.Storer.SetReference(branch) 466} 467 468func (w *Worktree) checkoutChangeSubmodule(name string, 469 a merkletrie.Action, 470 e *object.TreeEntry, 471 idx *indexBuilder, 472) error { 473 switch a { 474 case merkletrie.Modify: 475 sub, err := w.Submodule(name) 476 if err != nil { 477 return err 478 } 479 480 if !sub.initialized { 481 return nil 482 } 483 484 return w.addIndexFromTreeEntry(name, e, idx) 485 case merkletrie.Insert: 486 mode, err := e.Mode.ToOSFileMode() 487 if err != nil { 488 return err 489 } 490 491 if err := w.Filesystem.MkdirAll(name, mode); err != nil { 492 return err 493 } 494 495 return w.addIndexFromTreeEntry(name, e, idx) 496 } 497 498 return nil 499} 500 501func (w *Worktree) checkoutChangeRegularFile(name string, 502 a merkletrie.Action, 503 t *object.Tree, 504 e *object.TreeEntry, 505 idx *indexBuilder, 506) error { 507 switch a { 508 case merkletrie.Modify: 509 idx.Remove(name) 510 511 // to apply perm changes the file is deleted, billy doesn't implement 512 // chmod 513 if err := w.Filesystem.Remove(name); err != nil { 514 return err 515 } 516 517 fallthrough 518 case merkletrie.Insert: 519 f, err := t.File(name) 520 if err != nil { 521 return err 522 } 523 524 if err := w.checkoutFile(f); err != nil { 525 return err 526 } 527 528 return w.addIndexFromFile(name, e.Hash, idx) 529 } 530 531 return nil 532} 533 534func (w *Worktree) checkoutFile(f *object.File) (err error) { 535 mode, err := f.Mode.ToOSFileMode() 536 if err != nil { 537 return 538 } 539 540 if mode&os.ModeSymlink != 0 { 541 return w.checkoutFileSymlink(f) 542 } 543 544 from, err := f.Reader() 545 if err != nil { 546 return 547 } 548 549 defer ioutil.CheckClose(from, &err) 550 551 to, err := w.Filesystem.OpenFile(f.Name, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, mode.Perm()) 552 if err != nil { 553 return 554 } 555 556 defer ioutil.CheckClose(to, &err) 557 buf := sync.GetByteSlice() 558 _, err = io.CopyBuffer(to, from, *buf) 559 sync.PutByteSlice(buf) 560 return 561} 562 563func (w *Worktree) checkoutFileSymlink(f *object.File) (err error) { 564 from, err := f.Reader() 565 if err != nil { 566 return 567 } 568 569 defer ioutil.CheckClose(from, &err) 570 571 bytes, err := io.ReadAll(from) 572 if err != nil { 573 return 574 } 575 576 err = w.Filesystem.Symlink(string(bytes), f.Name) 577 578 // On windows, this might fail. 579 // Follow Git on Windows behavior by writing the link as it is. 580 if err != nil && isSymlinkWindowsNonAdmin(err) { 581 mode, _ := f.Mode.ToOSFileMode() 582 583 to, err := w.Filesystem.OpenFile(f.Name, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, mode.Perm()) 584 if err != nil { 585 return err 586 } 587 588 defer ioutil.CheckClose(to, &err) 589 590 _, err = to.Write(bytes) 591 return err 592 } 593 return 594} 595 596func (w *Worktree) addIndexFromTreeEntry(name string, f *object.TreeEntry, idx *indexBuilder) error { 597 idx.Remove(name) 598 idx.Add(&index.Entry{ 599 Hash: f.Hash, 600 Name: name, 601 Mode: filemode.Submodule, 602 }) 603 return nil 604} 605 606func (w *Worktree) addIndexFromFile(name string, h plumbing.Hash, idx *indexBuilder) error { 607 idx.Remove(name) 608 fi, err := w.Filesystem.Lstat(name) 609 if err != nil { 610 return err 611 } 612 613 mode, err := filemode.NewFromOSFileMode(fi.Mode()) 614 if err != nil { 615 return err 616 } 617 618 e := &index.Entry{ 619 Hash: h, 620 Name: name, 621 Mode: mode, 622 ModifiedAt: fi.ModTime(), 623 Size: uint32(fi.Size()), 624 } 625 626 // if the FileInfo.Sys() comes from os the ctime, dev, inode, uid and gid 627 // can be retrieved, otherwise this doesn't apply 628 if fillSystemInfo != nil { 629 fillSystemInfo(e, fi.Sys()) 630 } 631 idx.Add(e) 632 return nil 633} 634 635func (r *Repository) getTreeFromCommitHash(commit plumbing.Hash) (*object.Tree, error) { 636 c, err := r.CommitObject(commit) 637 if err != nil { 638 return nil, err 639 } 640 641 return c.Tree() 642} 643 644var fillSystemInfo func(e *index.Entry, sys interface{}) 645 646const gitmodulesFile = ".gitmodules" 647 648// Submodule returns the submodule with the given name 649func (w *Worktree) Submodule(name string) (*Submodule, error) { 650 l, err := w.Submodules() 651 if err != nil { 652 return nil, err 653 } 654 655 for _, m := range l { 656 if m.Config().Name == name { 657 return m, nil 658 } 659 } 660 661 return nil, ErrSubmoduleNotFound 662} 663 664// Submodules returns all the available submodules 665func (w *Worktree) Submodules() (Submodules, error) { 666 l := make(Submodules, 0) 667 m, err := w.readGitmodulesFile() 668 if err != nil || m == nil { 669 return l, err 670 } 671 672 c, err := w.r.Config() 673 if err != nil { 674 return nil, err 675 } 676 677 for _, s := range m.Submodules { 678 l = append(l, w.newSubmodule(s, c.Submodules[s.Name])) 679 } 680 681 return l, nil 682} 683 684func (w *Worktree) newSubmodule(fromModules, fromConfig *config.Submodule) *Submodule { 685 m := &Submodule{w: w} 686 m.initialized = fromConfig != nil 687 688 if !m.initialized { 689 m.c = fromModules 690 return m 691 } 692 693 m.c = fromConfig 694 m.c.Path = fromModules.Path 695 return m 696} 697 698func (w *Worktree) isSymlink(path string) bool { 699 if s, err := w.Filesystem.Lstat(path); err == nil { 700 return s.Mode()&os.ModeSymlink != 0 701 } 702 return false 703} 704 705func (w *Worktree) readGitmodulesFile() (*config.Modules, error) { 706 if w.isSymlink(gitmodulesFile) { 707 return nil, ErrGitModulesSymlink 708 } 709 710 f, err := w.Filesystem.Open(gitmodulesFile) 711 if err != nil { 712 if os.IsNotExist(err) { 713 return nil, nil 714 } 715 716 return nil, err 717 } 718 719 defer f.Close() 720 input, err := io.ReadAll(f) 721 if err != nil { 722 return nil, err 723 } 724 725 m := config.NewModules() 726 if err := m.Unmarshal(input); err != nil { 727 return m, err 728 } 729 730 return m, nil 731} 732 733// Clean the worktree by removing untracked files. 734// An empty dir could be removed - this is what `git clean -f -d .` does. 735func (w *Worktree) Clean(opts *CleanOptions) error { 736 s, err := w.Status() 737 if err != nil { 738 return err 739 } 740 741 root := "" 742 files, err := w.Filesystem.ReadDir(root) 743 if err != nil { 744 return err 745 } 746 return w.doClean(s, opts, root, files) 747} 748 749func (w *Worktree) doClean(status Status, opts *CleanOptions, dir string, files []os.FileInfo) error { 750 for _, fi := range files { 751 if fi.Name() == GitDirName { 752 continue 753 } 754 755 // relative path under the root 756 path := filepath.Join(dir, fi.Name()) 757 if fi.IsDir() { 758 if !opts.Dir { 759 continue 760 } 761 762 subfiles, err := w.Filesystem.ReadDir(path) 763 if err != nil { 764 return err 765 } 766 err = w.doClean(status, opts, path, subfiles) 767 if err != nil { 768 return err 769 } 770 } else { 771 if status.IsUntracked(path) { 772 if err := w.Filesystem.Remove(path); err != nil { 773 return err 774 } 775 } 776 } 777 } 778 779 if opts.Dir && dir != "" { 780 _, err := removeDirIfEmpty(w.Filesystem, dir) 781 return err 782 } 783 784 return nil 785} 786 787// GrepResult is structure of a grep result. 788type GrepResult struct { 789 // FileName is the name of file which contains match. 790 FileName string 791 // LineNumber is the line number of a file at which a match was found. 792 LineNumber int 793 // Content is the content of the file at the matching line. 794 Content string 795 // TreeName is the name of the tree (reference name/commit hash) at 796 // which the match was performed. 797 TreeName string 798} 799 800func (gr GrepResult) String() string { 801 return fmt.Sprintf("%s:%s:%d:%s", gr.TreeName, gr.FileName, gr.LineNumber, gr.Content) 802} 803 804// Grep performs grep on a repository. 805func (r *Repository) Grep(opts *GrepOptions) ([]GrepResult, error) { 806 if err := opts.validate(r); err != nil { 807 return nil, err 808 } 809 810 // Obtain commit hash from options (CommitHash or ReferenceName). 811 var commitHash plumbing.Hash 812 // treeName contains the value of TreeName in GrepResult. 813 var treeName string 814 815 if opts.ReferenceName != "" { 816 ref, err := r.Reference(opts.ReferenceName, true) 817 if err != nil { 818 return nil, err 819 } 820 commitHash = ref.Hash() 821 treeName = opts.ReferenceName.String() 822 } else if !opts.CommitHash.IsZero() { 823 commitHash = opts.CommitHash 824 treeName = opts.CommitHash.String() 825 } 826 827 // Obtain a tree from the commit hash and get a tracked files iterator from 828 // the tree. 829 tree, err := r.getTreeFromCommitHash(commitHash) 830 if err != nil { 831 return nil, err 832 } 833 fileiter := tree.Files() 834 835 return findMatchInFiles(fileiter, treeName, opts) 836} 837 838// Grep performs grep on a worktree. 839func (w *Worktree) Grep(opts *GrepOptions) ([]GrepResult, error) { 840 return w.r.Grep(opts) 841} 842 843// findMatchInFiles takes a FileIter, worktree name and GrepOptions, and 844// returns a slice of GrepResult containing the result of regex pattern matching 845// in content of all the files. 846func findMatchInFiles(fileiter *object.FileIter, treeName string, opts *GrepOptions) ([]GrepResult, error) { 847 var results []GrepResult 848 849 err := fileiter.ForEach(func(file *object.File) error { 850 var fileInPathSpec bool 851 852 // When no pathspecs are provided, search all the files. 853 if len(opts.PathSpecs) == 0 { 854 fileInPathSpec = true 855 } 856 857 // Check if the file name matches with the pathspec. Break out of the 858 // loop once a match is found. 859 for _, pathSpec := range opts.PathSpecs { 860 if pathSpec != nil && pathSpec.MatchString(file.Name) { 861 fileInPathSpec = true 862 break 863 } 864 } 865 866 // If the file does not match with any of the pathspec, skip it. 867 if !fileInPathSpec { 868 return nil 869 } 870 871 grepResults, err := findMatchInFile(file, treeName, opts) 872 if err != nil { 873 return err 874 } 875 results = append(results, grepResults...) 876 877 return nil 878 }) 879 880 return results, err 881} 882 883// findMatchInFile takes a single File, worktree name and GrepOptions, 884// and returns a slice of GrepResult containing the result of regex pattern 885// matching in the given file. 886func findMatchInFile(file *object.File, treeName string, opts *GrepOptions) ([]GrepResult, error) { 887 var grepResults []GrepResult 888 889 content, err := file.Contents() 890 if err != nil { 891 return grepResults, err 892 } 893 894 // Split the file content and parse line-by-line. 895 contentByLine := strings.Split(content, "\n") 896 for lineNum, cnt := range contentByLine { 897 addToResult := false 898 899 // Match the patterns and content. Break out of the loop once a 900 // match is found. 901 for _, pattern := range opts.Patterns { 902 if pattern != nil && pattern.MatchString(cnt) { 903 // Add to result only if invert match is not enabled. 904 if !opts.InvertMatch { 905 addToResult = true 906 break 907 } 908 } else if opts.InvertMatch { 909 // If matching fails, and invert match is enabled, add to 910 // results. 911 addToResult = true 912 break 913 } 914 } 915 916 if addToResult { 917 grepResults = append(grepResults, GrepResult{ 918 FileName: file.Name, 919 LineNumber: lineNum + 1, 920 Content: cnt, 921 TreeName: treeName, 922 }) 923 } 924 } 925 926 return grepResults, nil 927} 928 929// will walk up the directory tree removing all encountered empty 930// directories, not just the one containing this file 931func rmFileAndDirsIfEmpty(fs billy.Filesystem, name string) error { 932 if err := util.RemoveAll(fs, name); err != nil { 933 return err 934 } 935 936 dir := filepath.Dir(name) 937 for { 938 removed, err := removeDirIfEmpty(fs, dir) 939 if err != nil { 940 return err 941 } 942 943 if !removed { 944 // directory was not empty and not removed, 945 // stop checking parents 946 break 947 } 948 949 // move to parent directory 950 dir = filepath.Dir(dir) 951 } 952 953 return nil 954} 955 956// removeDirIfEmpty will remove the supplied directory `dir` if 957// `dir` is empty 958// returns true if the directory was removed 959func removeDirIfEmpty(fs billy.Filesystem, dir string) (bool, error) { 960 files, err := fs.ReadDir(dir) 961 if err != nil { 962 return false, err 963 } 964 965 if len(files) > 0 { 966 return false, nil 967 } 968 969 err = fs.Remove(dir) 970 if err != nil { 971 return false, err 972 } 973 974 return true, nil 975} 976 977type indexBuilder struct { 978 entries map[string]*index.Entry 979} 980 981func newIndexBuilder(idx *index.Index) *indexBuilder { 982 entries := make(map[string]*index.Entry, len(idx.Entries)) 983 for _, e := range idx.Entries { 984 entries[e.Name] = e 985 } 986 return &indexBuilder{ 987 entries: entries, 988 } 989} 990 991func (b *indexBuilder) Write(idx *index.Index) { 992 idx.Entries = idx.Entries[:0] 993 for _, e := range b.entries { 994 idx.Entries = append(idx.Entries, e) 995 } 996} 997 998func (b *indexBuilder) Add(e *index.Entry) { 999 b.entries[e.Name] = e 1000} 1001 1002func (b *indexBuilder) Remove(name string) { 1003 delete(b.entries, filepath.ToSlash(name)) 1004}