diff options
author | Máximo Cuadros <mcuadros@gmail.com> | 2017-01-29 02:17:01 +0100 |
---|---|---|
committer | Máximo Cuadros <mcuadros@gmail.com> | 2017-01-29 02:17:01 +0100 |
commit | 352170d1bf8cc7b32b85d8a2740eb3627e952a6e (patch) | |
tree | ba9cdd6b32b0871a78fcd91aee90a388dd3e2326 /worktree.go | |
parent | de1c0bfec0269ff2a31b62d13612d833da178ec4 (diff) | |
download | go-git-352170d1bf8cc7b32b85d8a2740eb3627e952a6e.tar.gz |
worktree, status implementation
Diffstat (limited to 'worktree.go')
-rw-r--r-- | worktree.go | 253 |
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 } |