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}