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