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}