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