aboutsummaryrefslogtreecommitdiffstats
path: root/worktree.go
diff options
context:
space:
mode:
authorMáximo Cuadros <mcuadros@gmail.com>2017-01-29 02:17:01 +0100
committerMáximo Cuadros <mcuadros@gmail.com>2017-01-29 02:17:01 +0100
commit352170d1bf8cc7b32b85d8a2740eb3627e952a6e (patch)
treeba9cdd6b32b0871a78fcd91aee90a388dd3e2326 /worktree.go
parentde1c0bfec0269ff2a31b62d13612d833da178ec4 (diff)
downloadgo-git-352170d1bf8cc7b32b85d8a2740eb3627e952a6e.tar.gz
worktree, status implementation
Diffstat (limited to 'worktree.go')
-rw-r--r--worktree.go253
1 files changed, 249 insertions, 4 deletions
diff --git a/worktree.go b/worktree.go
index 2aefa76..c786c95 100644
--- a/worktree.go
+++ b/worktree.go
@@ -1,21 +1,37 @@
package git
import (
+ "errors"
+ "fmt"
"io"
"os"
+ "syscall"
+ "time"
"gopkg.in/src-d/go-git.v4/plumbing"
+ "gopkg.in/src-d/go-git.v4/plumbing/format/index"
"gopkg.in/src-d/go-git.v4/plumbing/object"
"srcd.works/go-billy.v1"
)
+var ErrWorktreeNotClean = errors.New("worktree is not clean")
+
type Worktree struct {
r *Repository
fs billy.Filesystem
}
func (w *Worktree) Checkout(commit plumbing.Hash) error {
+ s, err := w.Status()
+ if err != nil {
+ return err
+ }
+
+ if !s.IsClean() {
+ return ErrWorktreeNotClean
+ }
+
c, err := w.r.Commit(commit)
if err != nil {
return err
@@ -26,17 +42,24 @@ func (w *Worktree) Checkout(commit plumbing.Hash) error {
return err
}
- return files.ForEach(w.checkoutFile)
+ idx := &index.Index{Version: 2}
+ if err := files.ForEach(func(f *object.File) error {
+ return w.checkoutFile(f, idx)
+ }); err != nil {
+ return err
+ }
+
+ return w.r.s.SetIndex(idx)
}
-func (w *Worktree) checkoutFile(f *object.File) error {
+func (w *Worktree) checkoutFile(f *object.File, idx *index.Index) error {
from, err := f.Reader()
if err != nil {
return err
}
defer from.Close()
- to, err := w.fs.OpenFile(f.Name, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, f.Mode)
+ to, err := w.fs.OpenFile(f.Name, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, f.Mode.Perm())
if err != nil {
return err
}
@@ -45,5 +68,227 @@ func (w *Worktree) checkoutFile(f *object.File) error {
return err
}
- return to.Close()
+ defer to.Close()
+ return w.indexFile(f, idx)
+}
+
+func (w *Worktree) indexFile(f *object.File, idx *index.Index) error {
+ fi, err := w.fs.Stat(f.Name)
+ if err != nil {
+ return err
+ }
+
+ e := index.Entry{
+ Hash: f.Hash,
+ Name: f.Name,
+ Mode: fi.Mode(),
+ ModifiedAt: fi.ModTime(),
+ Size: uint32(fi.Size()),
+ }
+
+ // if the FileInfo.Sys() comes from os the ctime, dev, inode, uid and gid
+ // can be retrieved, otherwise this doesn't apply
+ os, ok := fi.Sys().(*syscall.Stat_t)
+ if ok {
+ e.CreatedAt = time.Unix(int64(os.Ctim.Sec), int64(os.Ctim.Nsec))
+ e.Dev = uint32(os.Dev)
+ e.Inode = uint32(os.Ino)
+ e.GID = os.Gid
+ e.UID = os.Uid
+ }
+
+ idx.Entries = append(idx.Entries, e)
+ return nil
+}
+
+func (w *Worktree) Status() (Status, error) {
+ idx, err := w.r.s.Index()
+ if err != nil {
+ return nil, err
+ }
+
+ files, err := readDirAll(w.fs)
+ if err != nil {
+ return nil, err
+ }
+
+ s := make(Status, 0)
+ for _, e := range idx.Entries {
+ fi, ok := files[e.Name]
+ delete(files, e.Name)
+
+ if !ok {
+ s.File(e.Name).Worktree = Deleted
+ continue
+ }
+
+ status, err := w.compareFileWithEntry(fi, &e)
+ if err != nil {
+ return nil, err
+ }
+
+ s.File(e.Name).Worktree = status
+ }
+
+ for f := range files {
+ s.File(f).Worktree = Untracked
+ }
+
+ return s, nil
+}
+
+func (w *Worktree) compareFileWithEntry(fi billy.FileInfo, e *index.Entry) (StatusCode, error) {
+ if fi.Size() != int64(e.Size) {
+ return Modified, nil
+ }
+
+ if fi.Mode().Perm() != e.Mode.Perm() {
+ return Modified, nil
+ }
+
+ h, err := calcSHA1(w.fs, e.Name)
+ if h != e.Hash || err != nil {
+ return Modified, err
+
+ }
+
+ return Unmodified, nil
+}
+
+// Status current status of a Worktree
+type Status map[string]*FileStatus
+
+func (s Status) File(filename string) *FileStatus {
+ if _, ok := (s)[filename]; !ok {
+ s[filename] = &FileStatus{}
+ }
+
+ return s[filename]
+
+}
+
+func (s Status) IsClean() bool {
+ for _, status := range s {
+ if status.Worktree != Unmodified || status.Staging != Unmodified {
+ return false
+ }
+ }
+
+ return true
+}
+
+func (s Status) String() string {
+ var names []string
+ for name := range s {
+ names = append(names, name)
+ }
+
+ var output string
+ for _, name := range names {
+ status := s[name]
+ if status.Staging == 0 && status.Worktree == 0 {
+ continue
+ }
+
+ if status.Staging == Renamed {
+ name = fmt.Sprintf("%s -> %s", name, status.Extra)
+ }
+
+ output += fmt.Sprintf("%s%s %s\n", status.Staging, status.Worktree, name)
+ }
+
+ return output
+}
+
+// FileStatus status of a file in the Worktree
+type FileStatus struct {
+ Staging StatusCode
+ Worktree StatusCode
+ Extra string
+}
+
+// StatusCode status code of a file in the Worktree
+type StatusCode int8
+
+const (
+ Unmodified StatusCode = iota
+ Untracked
+ Modified
+ Added
+ Deleted
+ Renamed
+ Copied
+ UpdatedButUnmerged
+)
+
+func (c StatusCode) String() string {
+ switch c {
+ case Unmodified:
+ return " "
+ case Modified:
+ return "M"
+ case Added:
+ return "A"
+ case Deleted:
+ return "D"
+ case Renamed:
+ return "R"
+ case Copied:
+ return "C"
+ case UpdatedButUnmerged:
+ return "U"
+ case Untracked:
+ return "?"
+ default:
+ return "-"
+ }
+}
+
+func calcSHA1(fs billy.Filesystem, filename string) (plumbing.Hash, error) {
+ file, err := fs.Open(filename)
+ if err != nil {
+ return plumbing.ZeroHash, err
+ }
+
+ stat, err := fs.Stat(filename)
+ if err != nil {
+ return plumbing.ZeroHash, err
+ }
+
+ h := plumbing.NewHasher(plumbing.BlobObject, stat.Size())
+ if _, err := io.Copy(h, file); err != nil {
+ return plumbing.ZeroHash, err
+ }
+
+ return h.Sum(), nil
+}
+
+func readDirAll(filesystem billy.Filesystem) (map[string]billy.FileInfo, error) {
+ all := make(map[string]billy.FileInfo, 0)
+ return all, doReadDirAll(filesystem, "", all)
+}
+
+func doReadDirAll(fs billy.Filesystem, path string, files map[string]billy.FileInfo) error {
+ if path == ".git" {
+ return nil
+ }
+
+ l, err := fs.ReadDir(path)
+ if err != nil {
+ return err
+ }
+
+ for _, info := range l {
+ file := fs.Join(path, info.Name())
+ if !info.IsDir() {
+ files[file] = info
+ continue
+ }
+
+ if err := doReadDirAll(fs, file, files); err != nil {
+ return err
+ }
+ }
+
+ return nil
}