aboutsummaryrefslogblamecommitdiffstats
path: root/worktree_status.go
blob: 6becada3a41101c1f52ccd1e621641de955d7c9d (plain) (tree)
1
2
3
4
5
6
7
8
9



               
            

                                           

                                                        
                                                  
                                               

                                                              
                                                                


                                                         
                                          















                                                                 
                                                           



                               
                                 






                                       
                                                                  
                                       
                                                              
                                       
                                                                 


                 
                                                 



                               
                                  






                                       
                                                                   
                                       

                                                                   
                                       
                                                                  











                                                                          
                                       





                                                      
                                                              

 
























                                                                            





                                                                                                          









                                          
                                     
                                         
 
                    
                                                                      

         
                                                              

 












                                                                                     


                            
                                        
 





































































































                                                                                                  
package git

import (
	"bytes"
	"io"

	"gopkg.in/src-d/go-git.v4/plumbing"
	"gopkg.in/src-d/go-git.v4/plumbing/filemode"
	"gopkg.in/src-d/go-git.v4/plumbing/format/index"
	"gopkg.in/src-d/go-git.v4/plumbing/object"
	"gopkg.in/src-d/go-git.v4/utils/ioutil"
	"gopkg.in/src-d/go-git.v4/utils/merkletrie"
	"gopkg.in/src-d/go-git.v4/utils/merkletrie/filesystem"
	mindex "gopkg.in/src-d/go-git.v4/utils/merkletrie/index"
	"gopkg.in/src-d/go-git.v4/utils/merkletrie/noder"
)

// Status returns the working tree status.
func (w *Worktree) Status() (Status, error) {
	ref, err := w.r.Head()
	if err == plumbing.ErrReferenceNotFound {
		return nil, nil
	}

	if err != nil {
		return nil, err
	}

	return w.status(ref.Hash())
}

func (w *Worktree) status(commit plumbing.Hash) (Status, error) {
	s := make(Status, 0)

	left, err := w.diffCommitWithStaging(commit, false)
	if err != nil {
		return nil, err
	}

	for _, ch := range left {
		a, err := ch.Action()
		if err != nil {
			return nil, err
		}

		switch a {
		case merkletrie.Delete:
			s.File(ch.From.String()).Staging = Deleted
		case merkletrie.Insert:
			s.File(ch.To.String()).Staging = Added
		case merkletrie.Modify:
			s.File(ch.To.String()).Staging = Modified
		}
	}

	right, err := w.diffStagingWithWorktree()
	if err != nil {
		return nil, err
	}

	for _, ch := range right {
		a, err := ch.Action()
		if err != nil {
			return nil, err
		}

		switch a {
		case merkletrie.Delete:
			s.File(ch.From.String()).Worktree = Deleted
		case merkletrie.Insert:
			s.File(ch.To.String()).Worktree = Untracked
			s.File(ch.To.String()).Staging = Untracked
		case merkletrie.Modify:
			s.File(ch.To.String()).Worktree = Modified
		}
	}

	return s, nil
}

func (w *Worktree) diffStagingWithWorktree() (merkletrie.Changes, error) {
	idx, err := w.r.Storer.Index()
	if err != nil {
		return nil, err
	}

	from := mindex.NewRootNode(idx)
	submodules, err := w.getSubmodulesStatus()
	if err != nil {
		return nil, err
	}

	to := filesystem.NewRootNode(w.fs, submodules)
	return merkletrie.DiffTree(from, to, diffTreeIsEquals)
}

func (w *Worktree) getSubmodulesStatus() (map[string]plumbing.Hash, error) {
	o := map[string]plumbing.Hash{}

	sub, err := w.Submodules()
	if err != nil {
		return nil, err
	}

	status, err := sub.Status()
	if err != nil {
		return nil, err
	}

	for _, s := range status {
		if s.Current.IsZero() {
			o[s.Path] = s.Expected
			continue
		}

		o[s.Path] = s.Current
	}

	return o, nil
}

func (w *Worktree) diffCommitWithStaging(commit plumbing.Hash, reverse bool) (merkletrie.Changes, error) {
	idx, err := w.r.Storer.Index()
	if err != nil {
		return nil, err
	}

	c, err := w.r.CommitObject(commit)
	if err != nil {
		return nil, err
	}

	t, err := c.Tree()
	if err != nil {
		return nil, err
	}

	to := mindex.NewRootNode(idx)
	from := object.NewTreeRootNode(t)

	if reverse {
		return merkletrie.DiffTree(to, from, diffTreeIsEquals)
	}

	return merkletrie.DiffTree(from, to, diffTreeIsEquals)
}

var emptyNoderHash = make([]byte, 24)

// diffTreeIsEquals is a implementation of noder.Equals, used to compare
// noder.Noder, it compare the content and the length of the hashes.
//
// Since some of the noder.Noder implementations doesn't compute a hash for
// some directories, if any of the hashes is a 24-byte slice of zero values
// the comparison is not done and the hashes are take as different.
func diffTreeIsEquals(a, b noder.Hasher) bool {
	hashA := a.Hash()
	hashB := b.Hash()

	if bytes.Equal(hashA, emptyNoderHash) || bytes.Equal(hashB, emptyNoderHash) {
		return false
	}

	return bytes.Equal(hashA, hashB)
}

// Add adds the file contents of a file in the worktree to the index. if the
// file is already stagged in the index no error is returned.
func (w *Worktree) Add(path string) (plumbing.Hash, error) {
	s, err := w.Status()
	if err != nil {
		return plumbing.ZeroHash, err
	}

	h, err := w.calculateBlobHash(path)
	if err != nil {
		return h, err
	}

	if s.File(path).Worktree == Unmodified {
		return h, nil
	}

	if err := w.addOrUpdateFileToIndex(path, h); err != nil {
		return h, err
	}

	return h, err
}

func (w *Worktree) calculateBlobHash(filename string) (hash plumbing.Hash, err error) {
	fi, err := w.fs.Stat(filename)
	if err != nil {
		return plumbing.ZeroHash, err
	}

	f, err := w.fs.Open(filename)
	if err != nil {
		return plumbing.ZeroHash, err
	}

	defer ioutil.CheckClose(f, &err)

	h := plumbing.NewHasher(plumbing.BlobObject, fi.Size())
	if _, err := io.Copy(h, f); err != nil {
		return plumbing.ZeroHash, err
	}

	hash = h.Sum()
	return
}

func (w *Worktree) addOrUpdateFileToIndex(filename string, h plumbing.Hash) error {
	idx, err := w.r.Storer.Index()
	if err != nil {
		return err
	}

	_, err = idx.Entry(filename)
	if err == index.ErrEntryNotFound {
		err = w.doAddFileToIndex(idx, filename)
	}

	if err != nil {
		return err
	}

	err = w.doUpdateFileToIndex(idx, filename, h)
	if err != nil {
		return err
	}

	return w.r.Storer.SetIndex(idx)
}

func (w *Worktree) doAddFileToIndex(idx *index.Index, filename string) error {
	idx.Entries = append(idx.Entries, index.Entry{
		Name: filename,
	})

	return nil
}

func (w *Worktree) doUpdateFileToIndex(idx *index.Index, filename string, h plumbing.Hash) error {
	info, err := w.fs.Stat(filename)
	if err != nil {
		return err
	}

	for i, e := range idx.Entries {
		if e.Name != filename {
			continue
		}

		e.Hash = h
		e.ModifiedAt = info.ModTime()
		e.Mode, err = filemode.NewFromOSFileMode(info.Mode())
		if err != nil {
			return err
		}

		fillSystemInfo(&e, info.Sys())
		idx.Entries[i] = e
	}

	return nil
}