1package git
2
3import (
4 "bytes"
5 "errors"
6 "io"
7 "os"
8 "path"
9 "path/filepath"
10 "strings"
11
12 "github.com/go-git/go-billy/v5/util"
13 "github.com/go-git/go-git/v5/plumbing"
14 "github.com/go-git/go-git/v5/plumbing/filemode"
15 "github.com/go-git/go-git/v5/plumbing/format/gitignore"
16 "github.com/go-git/go-git/v5/plumbing/format/index"
17 "github.com/go-git/go-git/v5/plumbing/object"
18 "github.com/go-git/go-git/v5/utils/ioutil"
19 "github.com/go-git/go-git/v5/utils/merkletrie"
20 "github.com/go-git/go-git/v5/utils/merkletrie/filesystem"
21 mindex "github.com/go-git/go-git/v5/utils/merkletrie/index"
22 "github.com/go-git/go-git/v5/utils/merkletrie/noder"
23)
24
25var (
26 // ErrDestinationExists in an Move operation means that the target exists on
27 // the worktree.
28 ErrDestinationExists = errors.New("destination exists")
29 // ErrGlobNoMatches in an AddGlob if the glob pattern does not match any
30 // files in the worktree.
31 ErrGlobNoMatches = errors.New("glob pattern did not match any files")
32 // ErrUnsupportedStatusStrategy occurs when an invalid StatusStrategy is used
33 // when processing the Worktree status.
34 ErrUnsupportedStatusStrategy = errors.New("unsupported status strategy")
35)
36
37// Status returns the working tree status.
38func (w *Worktree) Status() (Status, error) {
39 return w.StatusWithOptions(StatusOptions{Strategy: defaultStatusStrategy})
40}
41
42// StatusOptions defines the options for Worktree.StatusWithOptions().
43type StatusOptions struct {
44 Strategy StatusStrategy
45}
46
47// StatusWithOptions returns the working tree status.
48func (w *Worktree) StatusWithOptions(o StatusOptions) (Status, error) {
49 var hash plumbing.Hash
50
51 ref, err := w.r.Head()
52 if err != nil && err != plumbing.ErrReferenceNotFound {
53 return nil, err
54 }
55
56 if err == nil {
57 hash = ref.Hash()
58 }
59
60 return w.status(o.Strategy, hash)
61}
62
63func (w *Worktree) status(ss StatusStrategy, commit plumbing.Hash) (Status, error) {
64 s, err := ss.new(w)
65 if err != nil {
66 return nil, err
67 }
68
69 left, err := w.diffCommitWithStaging(commit, false)
70 if err != nil {
71 return nil, err
72 }
73
74 for _, ch := range left {
75 a, err := ch.Action()
76 if err != nil {
77 return nil, err
78 }
79
80 fs := s.File(nameFromAction(&ch))
81 fs.Worktree = Unmodified
82
83 switch a {
84 case merkletrie.Delete:
85 s.File(ch.From.String()).Staging = Deleted
86 case merkletrie.Insert:
87 s.File(ch.To.String()).Staging = Added
88 case merkletrie.Modify:
89 s.File(ch.To.String()).Staging = Modified
90 }
91 }
92
93 right, err := w.diffStagingWithWorktree(false, true)
94 if err != nil {
95 return nil, err
96 }
97
98 for _, ch := range right {
99 a, err := ch.Action()
100 if err != nil {
101 return nil, err
102 }
103
104 fs := s.File(nameFromAction(&ch))
105 if fs.Staging == Untracked {
106 fs.Staging = Unmodified
107 }
108
109 switch a {
110 case merkletrie.Delete:
111 fs.Worktree = Deleted
112 case merkletrie.Insert:
113 fs.Worktree = Untracked
114 fs.Staging = Untracked
115 case merkletrie.Modify:
116 fs.Worktree = Modified
117 }
118 }
119
120 return s, nil
121}
122
123func nameFromAction(ch *merkletrie.Change) string {
124 name := ch.To.String()
125 if name == "" {
126 return ch.From.String()
127 }
128
129 return name
130}
131
132func (w *Worktree) diffStagingWithWorktree(reverse, excludeIgnoredChanges bool) (merkletrie.Changes, error) {
133 idx, err := w.r.Storer.Index()
134 if err != nil {
135 return nil, err
136 }
137
138 from := mindex.NewRootNode(idx)
139 submodules, err := w.getSubmodulesStatus()
140 if err != nil {
141 return nil, err
142 }
143
144 to := filesystem.NewRootNode(w.Filesystem, submodules)
145
146 var c merkletrie.Changes
147 if reverse {
148 c, err = merkletrie.DiffTree(to, from, diffTreeIsEquals)
149 } else {
150 c, err = merkletrie.DiffTree(from, to, diffTreeIsEquals)
151 }
152
153 if err != nil {
154 return nil, err
155 }
156
157 if excludeIgnoredChanges {
158 return w.excludeIgnoredChanges(c), nil
159 }
160 return c, nil
161}
162
163func (w *Worktree) excludeIgnoredChanges(changes merkletrie.Changes) merkletrie.Changes {
164 patterns, err := gitignore.ReadPatterns(w.Filesystem, nil)
165 if err != nil {
166 return changes
167 }
168
169 patterns = append(patterns, w.Excludes...)
170
171 if len(patterns) == 0 {
172 return changes
173 }
174
175 m := gitignore.NewMatcher(patterns)
176
177 var res merkletrie.Changes
178 for _, ch := range changes {
179 var path []string
180 for _, n := range ch.To {
181 path = append(path, n.Name())
182 }
183 if len(path) == 0 {
184 for _, n := range ch.From {
185 path = append(path, n.Name())
186 }
187 }
188 if len(path) != 0 {
189 isDir := (len(ch.To) > 0 && ch.To.IsDir()) || (len(ch.From) > 0 && ch.From.IsDir())
190 if m.Match(path, isDir) {
191 if len(ch.From) == 0 {
192 continue
193 }
194 }
195 }
196 res = append(res, ch)
197 }
198 return res
199}
200
201func (w *Worktree) getSubmodulesStatus() (map[string]plumbing.Hash, error) {
202 o := map[string]plumbing.Hash{}
203
204 sub, err := w.Submodules()
205 if err != nil {
206 return nil, err
207 }
208
209 status, err := sub.Status()
210 if err != nil {
211 return nil, err
212 }
213
214 for _, s := range status {
215 if s.Current.IsZero() {
216 o[s.Path] = s.Expected
217 continue
218 }
219
220 o[s.Path] = s.Current
221 }
222
223 return o, nil
224}
225
226func (w *Worktree) diffCommitWithStaging(commit plumbing.Hash, reverse bool) (merkletrie.Changes, error) {
227 var t *object.Tree
228 if !commit.IsZero() {
229 c, err := w.r.CommitObject(commit)
230 if err != nil {
231 return nil, err
232 }
233
234 t, err = c.Tree()
235 if err != nil {
236 return nil, err
237 }
238 }
239
240 return w.diffTreeWithStaging(t, reverse)
241}
242
243func (w *Worktree) diffTreeWithStaging(t *object.Tree, reverse bool) (merkletrie.Changes, error) {
244 var from noder.Noder
245 if t != nil {
246 from = object.NewTreeRootNode(t)
247 }
248
249 idx, err := w.r.Storer.Index()
250 if err != nil {
251 return nil, err
252 }
253
254 to := mindex.NewRootNode(idx)
255
256 if reverse {
257 return merkletrie.DiffTree(to, from, diffTreeIsEquals)
258 }
259
260 return merkletrie.DiffTree(from, to, diffTreeIsEquals)
261}
262
263var emptyNoderHash = make([]byte, 24)
264
265// diffTreeIsEquals is a implementation of noder.Equals, used to compare
266// noder.Noder, it compare the content and the length of the hashes.
267//
268// Since some of the noder.Noder implementations doesn't compute a hash for
269// some directories, if any of the hashes is a 24-byte slice of zero values
270// the comparison is not done and the hashes are take as different.
271func diffTreeIsEquals(a, b noder.Hasher) bool {
272 hashA := a.Hash()
273 hashB := b.Hash()
274
275 if bytes.Equal(hashA, emptyNoderHash) || bytes.Equal(hashB, emptyNoderHash) {
276 return false
277 }
278
279 return bytes.Equal(hashA, hashB)
280}
281
282// Add adds the file contents of a file in the worktree to the index. if the
283// file is already staged in the index no error is returned. If a file deleted
284// from the Workspace is given, the file is removed from the index. If a
285// directory given, adds the files and all his sub-directories recursively in
286// the worktree to the index. If any of the files is already staged in the index
287// no error is returned. When path is a file, the blob.Hash is returned.
288func (w *Worktree) Add(path string) (plumbing.Hash, error) {
289 // TODO(mcuadros): deprecate in favor of AddWithOption in v6.
290 return w.doAdd(path, make([]gitignore.Pattern, 0), false)
291}
292
293func (w *Worktree) doAddDirectory(idx *index.Index, s Status, directory string, ignorePattern []gitignore.Pattern) (added bool, err error) {
294 if len(ignorePattern) > 0 {
295 m := gitignore.NewMatcher(ignorePattern)
296 matchPath := strings.Split(directory, string(os.PathSeparator))
297 if m.Match(matchPath, true) {
298 // ignore
299 return false, nil
300 }
301 }
302
303 directory = filepath.ToSlash(filepath.Clean(directory))
304
305 for name := range s {
306 if !isPathInDirectory(name, directory) {
307 continue
308 }
309
310 var a bool
311 a, _, err = w.doAddFile(idx, s, name, ignorePattern)
312 if err != nil {
313 return
314 }
315
316 added = added || a
317 }
318
319 return
320}
321
322func isPathInDirectory(path, directory string) bool {
323 return directory == "." || strings.HasPrefix(path, directory+"/")
324}
325
326// AddWithOptions file contents to the index, updates the index using the
327// current content found in the working tree, to prepare the content staged for
328// the next commit.
329//
330// It typically adds the current content of existing paths as a whole, but with
331// some options it can also be used to add content with only part of the changes
332// made to the working tree files applied, or remove paths that do not exist in
333// the working tree anymore.
334func (w *Worktree) AddWithOptions(opts *AddOptions) error {
335 if err := opts.Validate(w.r); err != nil {
336 return err
337 }
338
339 if opts.All {
340 _, err := w.doAdd(".", w.Excludes, false)
341 return err
342 }
343
344 if opts.Glob != "" {
345 return w.AddGlob(opts.Glob)
346 }
347
348 _, err := w.doAdd(opts.Path, make([]gitignore.Pattern, 0), opts.SkipStatus)
349 return err
350}
351
352func (w *Worktree) doAdd(path string, ignorePattern []gitignore.Pattern, skipStatus bool) (plumbing.Hash, error) {
353 idx, err := w.r.Storer.Index()
354 if err != nil {
355 return plumbing.ZeroHash, err
356 }
357
358 var h plumbing.Hash
359 var added bool
360
361 fi, err := w.Filesystem.Lstat(path)
362
363 // status is required for doAddDirectory
364 var s Status
365 var err2 error
366 if !skipStatus || fi == nil || fi.IsDir() {
367 s, err2 = w.Status()
368 if err2 != nil {
369 return plumbing.ZeroHash, err2
370 }
371 }
372
373 path = filepath.Clean(path)
374
375 if err != nil || !fi.IsDir() {
376 added, h, err = w.doAddFile(idx, s, path, ignorePattern)
377 } else {
378 added, err = w.doAddDirectory(idx, s, path, ignorePattern)
379 }
380
381 if err != nil {
382 return h, err
383 }
384
385 if !added {
386 return h, nil
387 }
388
389 return h, w.r.Storer.SetIndex(idx)
390}
391
392// AddGlob adds all paths, matching pattern, to the index. If pattern matches a
393// directory path, all directory contents are added to the index recursively. No
394// error is returned if all matching paths are already staged in index.
395func (w *Worktree) AddGlob(pattern string) error {
396 // TODO(mcuadros): deprecate in favor of AddWithOption in v6.
397 files, err := util.Glob(w.Filesystem, pattern)
398 if err != nil {
399 return err
400 }
401
402 if len(files) == 0 {
403 return ErrGlobNoMatches
404 }
405
406 s, err := w.Status()
407 if err != nil {
408 return err
409 }
410
411 idx, err := w.r.Storer.Index()
412 if err != nil {
413 return err
414 }
415
416 var saveIndex bool
417 for _, file := range files {
418 fi, err := w.Filesystem.Lstat(file)
419 if err != nil {
420 return err
421 }
422
423 var added bool
424 if fi.IsDir() {
425 added, err = w.doAddDirectory(idx, s, file, make([]gitignore.Pattern, 0))
426 } else {
427 added, _, err = w.doAddFile(idx, s, file, make([]gitignore.Pattern, 0))
428 }
429
430 if err != nil {
431 return err
432 }
433
434 if !saveIndex && added {
435 saveIndex = true
436 }
437 }
438
439 if saveIndex {
440 return w.r.Storer.SetIndex(idx)
441 }
442
443 return nil
444}
445
446// doAddFile create a new blob from path and update the index, added is true if
447// the file added is different from the index.
448// if s status is nil will skip the status check and update the index anyway
449func (w *Worktree) doAddFile(idx *index.Index, s Status, path string, ignorePattern []gitignore.Pattern) (added bool, h plumbing.Hash, err error) {
450 if s != nil && s.File(path).Worktree == Unmodified {
451 return false, h, nil
452 }
453 if len(ignorePattern) > 0 {
454 m := gitignore.NewMatcher(ignorePattern)
455 matchPath := strings.Split(path, string(os.PathSeparator))
456 if m.Match(matchPath, true) {
457 // ignore
458 return false, h, nil
459 }
460 }
461
462 h, err = w.copyFileToStorage(path)
463 if err != nil {
464 if os.IsNotExist(err) {
465 added = true
466 h, err = w.deleteFromIndex(idx, path)
467 }
468
469 return
470 }
471
472 if err := w.addOrUpdateFileToIndex(idx, path, h); err != nil {
473 return false, h, err
474 }
475
476 return true, h, err
477}
478
479func (w *Worktree) copyFileToStorage(path string) (hash plumbing.Hash, err error) {
480 fi, err := w.Filesystem.Lstat(path)
481 if err != nil {
482 return plumbing.ZeroHash, err
483 }
484
485 obj := w.r.Storer.NewEncodedObject()
486 obj.SetType(plumbing.BlobObject)
487 obj.SetSize(fi.Size())
488
489 writer, err := obj.Writer()
490 if err != nil {
491 return plumbing.ZeroHash, err
492 }
493
494 defer ioutil.CheckClose(writer, &err)
495
496 if fi.Mode()&os.ModeSymlink != 0 {
497 err = w.fillEncodedObjectFromSymlink(writer, path, fi)
498 } else {
499 err = w.fillEncodedObjectFromFile(writer, path, fi)
500 }
501
502 if err != nil {
503 return plumbing.ZeroHash, err
504 }
505
506 return w.r.Storer.SetEncodedObject(obj)
507}
508
509func (w *Worktree) fillEncodedObjectFromFile(dst io.Writer, path string, _ os.FileInfo) (err error) {
510 src, err := w.Filesystem.Open(path)
511 if err != nil {
512 return err
513 }
514
515 defer ioutil.CheckClose(src, &err)
516
517 if _, err := io.Copy(dst, src); err != nil {
518 return err
519 }
520
521 return err
522}
523
524func (w *Worktree) fillEncodedObjectFromSymlink(dst io.Writer, path string, _ os.FileInfo) error {
525 target, err := w.Filesystem.Readlink(path)
526 if err != nil {
527 return err
528 }
529
530 _, err = dst.Write([]byte(target))
531 return err
532}
533
534func (w *Worktree) addOrUpdateFileToIndex(idx *index.Index, filename string, h plumbing.Hash) error {
535 e, err := idx.Entry(filename)
536 if err != nil && err != index.ErrEntryNotFound {
537 return err
538 }
539
540 if err == index.ErrEntryNotFound {
541 return w.doAddFileToIndex(idx, filename, h)
542 }
543
544 return w.doUpdateFileToIndex(e, filename, h)
545}
546
547func (w *Worktree) doAddFileToIndex(idx *index.Index, filename string, h plumbing.Hash) error {
548 return w.doUpdateFileToIndex(idx.Add(filename), filename, h)
549}
550
551func (w *Worktree) doUpdateFileToIndex(e *index.Entry, filename string, h plumbing.Hash) error {
552 info, err := w.Filesystem.Lstat(filename)
553 if err != nil {
554 return err
555 }
556
557 e.Hash = h
558 e.ModifiedAt = info.ModTime()
559 e.Mode, err = filemode.NewFromOSFileMode(info.Mode())
560 if err != nil {
561 return err
562 }
563
564 // The entry size must always reflect the current state, otherwise
565 // it will cause go-git's Worktree.Status() to divert from "git status".
566 // The size of a symlink is the length of the path to the target.
567 // The size of Regular and Executable files is the size of the files.
568 e.Size = uint32(info.Size())
569
570 fillSystemInfo(e, info.Sys())
571 return nil
572}
573
574// Remove removes files from the working tree and from the index.
575func (w *Worktree) Remove(path string) (plumbing.Hash, error) {
576 // TODO(mcuadros): remove plumbing.Hash from signature at v5.
577 idx, err := w.r.Storer.Index()
578 if err != nil {
579 return plumbing.ZeroHash, err
580 }
581
582 var h plumbing.Hash
583
584 fi, err := w.Filesystem.Lstat(path)
585 if err != nil || !fi.IsDir() {
586 h, err = w.doRemoveFile(idx, path)
587 } else {
588 _, err = w.doRemoveDirectory(idx, path)
589 }
590 if err != nil {
591 return h, err
592 }
593
594 return h, w.r.Storer.SetIndex(idx)
595}
596
597func (w *Worktree) doRemoveDirectory(idx *index.Index, directory string) (removed bool, err error) {
598 files, err := w.Filesystem.ReadDir(directory)
599 if err != nil {
600 return false, err
601 }
602
603 for _, file := range files {
604 name := path.Join(directory, file.Name())
605
606 var r bool
607 if file.IsDir() {
608 r, err = w.doRemoveDirectory(idx, name)
609 } else {
610 _, err = w.doRemoveFile(idx, name)
611 if err == index.ErrEntryNotFound {
612 err = nil
613 }
614 }
615
616 if err != nil {
617 return
618 }
619
620 if !removed && r {
621 removed = true
622 }
623 }
624
625 err = w.removeEmptyDirectory(directory)
626 return
627}
628
629func (w *Worktree) removeEmptyDirectory(path string) error {
630 files, err := w.Filesystem.ReadDir(path)
631 if err != nil {
632 return err
633 }
634
635 if len(files) != 0 {
636 return nil
637 }
638
639 return w.Filesystem.Remove(path)
640}
641
642func (w *Worktree) doRemoveFile(idx *index.Index, path string) (plumbing.Hash, error) {
643 hash, err := w.deleteFromIndex(idx, path)
644 if err != nil {
645 return plumbing.ZeroHash, err
646 }
647
648 return hash, w.deleteFromFilesystem(path)
649}
650
651func (w *Worktree) deleteFromIndex(idx *index.Index, path string) (plumbing.Hash, error) {
652 e, err := idx.Remove(path)
653 if err != nil {
654 return plumbing.ZeroHash, err
655 }
656
657 return e.Hash, nil
658}
659
660func (w *Worktree) deleteFromFilesystem(path string) error {
661 err := w.Filesystem.Remove(path)
662 if os.IsNotExist(err) {
663 return nil
664 }
665
666 return err
667}
668
669// RemoveGlob removes all paths, matching pattern, from the index. If pattern
670// matches a directory path, all directory contents are removed from the index
671// recursively.
672func (w *Worktree) RemoveGlob(pattern string) error {
673 idx, err := w.r.Storer.Index()
674 if err != nil {
675 return err
676 }
677
678 entries, err := idx.Glob(pattern)
679 if err != nil {
680 return err
681 }
682
683 for _, e := range entries {
684 file := filepath.FromSlash(e.Name)
685 if _, err := w.Filesystem.Lstat(file); err != nil && !os.IsNotExist(err) {
686 return err
687 }
688
689 if _, err := w.doRemoveFile(idx, file); err != nil {
690 return err
691 }
692
693 dir, _ := filepath.Split(file)
694 if err := w.removeEmptyDirectory(dir); err != nil {
695 return err
696 }
697 }
698
699 return w.r.Storer.SetIndex(idx)
700}
701
702// Move moves or rename a file in the worktree and the index, directories are
703// not supported.
704func (w *Worktree) Move(from, to string) (plumbing.Hash, error) {
705 // TODO(mcuadros): support directories and/or implement support for glob
706 if _, err := w.Filesystem.Lstat(from); err != nil {
707 return plumbing.ZeroHash, err
708 }
709
710 if _, err := w.Filesystem.Lstat(to); err == nil {
711 return plumbing.ZeroHash, ErrDestinationExists
712 }
713
714 idx, err := w.r.Storer.Index()
715 if err != nil {
716 return plumbing.ZeroHash, err
717 }
718
719 hash, err := w.deleteFromIndex(idx, from)
720 if err != nil {
721 return plumbing.ZeroHash, err
722 }
723
724 if err := w.Filesystem.Rename(from, to); err != nil {
725 return hash, err
726 }
727
728 if err := w.addOrUpdateFileToIndex(idx, to, hash); err != nil {
729 return hash, err
730 }
731
732 return hash, w.r.Storer.SetIndex(idx)
733}