diff options
-rw-r--r-- | common_test.go | 1 | ||||
-rw-r--r-- | examples/clone/main.go | 5 | ||||
-rw-r--r-- | fixtures/fixtures.go | 2 | ||||
-rw-r--r-- | worktree.go | 253 | ||||
-rw-r--r-- | worktree_test.go | 129 |
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) } |