aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--common_test.go1
-rw-r--r--examples/clone/main.go5
-rw-r--r--fixtures/fixtures.go2
-rw-r--r--worktree.go253
-rw-r--r--worktree_test.go129
5 files changed, 380 insertions, 10 deletions
diff --git a/common_test.go b/common_test.go
index 8a6cb3d..783a99b 100644
--- a/common_test.go
+++ b/common_test.go
@@ -50,7 +50,6 @@ func (s *BaseSuite) buildBasicRepository(c *C) {
}
func (s *BaseSuite) NewRepository(f *fixtures.Fixture) *Repository {
-
fs := os.New(f.DotGit().Base())
st, err := filesystem.NewStorage(fs)
if err != nil {
diff --git a/examples/clone/main.go b/examples/clone/main.go
index 13257ca..fc50960 100644
--- a/examples/clone/main.go
+++ b/examples/clone/main.go
@@ -13,13 +13,10 @@ func main() {
url := os.Args[1]
directory := os.Args[2]
- r, err := git.NewFilesystemRepository(directory)
- CheckIfError(err)
-
// Clone the given repository to the given directory
Info("git clone %s %s", url, directory)
- err = r.Clone(&git.CloneOptions{
+ r, err := git.PlainClone(directory, false, &git.CloneOptions{
URL: url,
Depth: 1,
})
diff --git a/fixtures/fixtures.go b/fixtures/fixtures.go
index f79e107..5bcb80d 100644
--- a/fixtures/fixtures.go
+++ b/fixtures/fixtures.go
@@ -67,7 +67,7 @@ var fixtures = Fixtures{{
URL: "https://github.com/git-fixtures/basic.git",
DotGitHash: plumbing.NewHash("935e5ac17c41c309c356639816ea0694a568c484"),
}, {
- Tags: []string{".git", "worktree"},
+ Tags: []string{"worktree"},
URL: "https://github.com/git-fixtures/basic.git",
WorktreeHash: plumbing.NewHash("e4413db6700d0e72e7680b17c3d5ebbc2d1861bc"),
}, {
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
}
diff --git a/worktree_test.go b/worktree_test.go
index 9c5d8e3..62b0a03 100644
--- a/worktree_test.go
+++ b/worktree_test.go
@@ -1,10 +1,15 @@
package git
import (
+ "fmt"
"io/ioutil"
+ "os"
+
+ "gopkg.in/src-d/go-git.v4/plumbing/format/index"
. "gopkg.in/check.v1"
"srcd.works/go-billy.v1/memory"
+ osfs "srcd.works/go-billy.v1/os"
)
type WorktreeSuite struct {
@@ -13,6 +18,12 @@ type WorktreeSuite struct {
var _ = Suite(&WorktreeSuite{})
+func (s *WorktreeSuite) SetUpTest(c *C) {
+ s.buildBasicRepository(c)
+ // the index is removed if not the Repository will be not clean
+ c.Assert(s.Repository.s.SetIndex(&index.Index{Version: 2}), IsNil)
+}
+
func (s *WorktreeSuite) TestCheckout(c *C) {
h, err := s.Repository.Head()
c.Assert(err, IsNil)
@@ -36,4 +47,122 @@ func (s *WorktreeSuite) TestCheckout(c *C) {
content, err := ioutil.ReadAll(ch)
c.Assert(err, IsNil)
c.Assert(string(content), Equals, "Initial changelog\n")
+
+ idx, err := s.Repository.s.Index()
+ c.Assert(err, IsNil)
+ c.Assert(idx.Entries, HasLen, 9)
+}
+
+func (s *WorktreeSuite) TestCheckoutIndexMemory(c *C) {
+ h, err := s.Repository.Head()
+ c.Assert(err, IsNil)
+
+ fs := memory.New()
+ w := &Worktree{
+ r: s.Repository,
+ fs: fs,
+ }
+
+ err = w.Checkout(h.Hash())
+ c.Assert(err, IsNil)
+
+ idx, err := s.Repository.s.Index()
+ c.Assert(err, IsNil)
+ c.Assert(idx.Entries, HasLen, 9)
+ c.Assert(idx.Entries[0].Hash.String(), Equals, "32858aad3c383ed1ff0a0f9bdf231d54a00c9e88")
+ c.Assert(idx.Entries[0].Name, Equals, ".gitignore")
+ // in memoryfs the perms are not supported
+ c.Assert(idx.Entries[0].Mode, Equals, os.FileMode(0))
+ c.Assert(idx.Entries[0].ModifiedAt.IsZero(), Equals, false)
+ c.Assert(idx.Entries[0].Size, Equals, uint32(189))
+
+ // ctime, dev, inode, uid and gid are not supported on memory fs
+ c.Assert(idx.Entries[0].CreatedAt.IsZero(), Equals, true)
+ c.Assert(idx.Entries[0].Dev, Equals, uint32(0))
+ c.Assert(idx.Entries[0].Inode, Equals, uint32(0))
+ c.Assert(idx.Entries[0].UID, Equals, uint32(0))
+ c.Assert(idx.Entries[0].GID, Equals, uint32(0))
+}
+
+func (s *WorktreeSuite) TestCheckoutIndexOS(c *C) {
+ h, err := s.Repository.Head()
+ c.Assert(err, IsNil)
+
+ dir, err := ioutil.TempDir("", "checkout")
+ defer os.RemoveAll(dir)
+
+ fs := osfs.New(dir)
+ w := &Worktree{
+ r: s.Repository,
+ fs: fs,
+ }
+
+ err = w.Checkout(h.Hash())
+ c.Assert(err, IsNil)
+
+ idx, err := s.Repository.s.Index()
+ c.Assert(err, IsNil)
+ c.Assert(idx.Entries, HasLen, 9)
+ c.Assert(idx.Entries[0].Hash.String(), Equals, "32858aad3c383ed1ff0a0f9bdf231d54a00c9e88")
+ c.Assert(idx.Entries[0].Name, Equals, ".gitignore")
+ c.Assert(idx.Entries[0].Mode, Equals, os.FileMode(0644))
+ c.Assert(idx.Entries[0].ModifiedAt.IsZero(), Equals, false)
+ c.Assert(idx.Entries[0].Size, Equals, uint32(189))
+
+ c.Assert(idx.Entries[0].CreatedAt.IsZero(), Equals, false)
+ c.Assert(idx.Entries[0].Dev, Not(Equals), uint32(0))
+ c.Assert(idx.Entries[0].Inode, Not(Equals), uint32(0))
+ c.Assert(idx.Entries[0].UID, Not(Equals), uint32(0))
+ c.Assert(idx.Entries[0].GID, Not(Equals), uint32(0))
+}
+
+func (s *WorktreeSuite) TestStatus(c *C) {
+
+ h, err := s.Repository.Head()
+ c.Assert(err, IsNil)
+
+ fs := memory.New()
+ w := &Worktree{
+ r: s.Repository,
+ fs: fs,
+ }
+
+ err = w.Checkout(h.Hash())
+ c.Assert(err, IsNil)
+
+ status, err := w.Status()
+ c.Assert(err, IsNil)
+
+ fmt.Println(status)
+ c.Assert(status.IsClean(), Equals, true)
+}
+
+func (s *WorktreeSuite) TestStatusModified(c *C) {
+ c.Assert(s.Repository.s.SetIndex(&index.Index{Version: 2}), IsNil)
+
+ h, err := s.Repository.Head()
+ c.Assert(err, IsNil)
+
+ dir, err := ioutil.TempDir("", "status")
+ defer os.RemoveAll(dir)
+
+ fs := osfs.New(dir)
+ w := &Worktree{
+ r: s.Repository,
+ fs: fs,
+ }
+
+ err = w.Checkout(h.Hash())
+ c.Assert(err, IsNil)
+
+ f, err := fs.Create(".gitignore")
+ c.Assert(err, IsNil)
+ _, err = f.Write([]byte("foo"))
+ c.Assert(err, IsNil)
+ err = f.Close()
+ c.Assert(err, IsNil)
+
+ status, err := w.Status()
+ c.Assert(err, IsNil)
+ c.Assert(status.IsClean(), Equals, false)
}