Skip to content
This repository was archived by the owner on Sep 11, 2020. It is now read-only.

worktree, status and reset implementation based on merkletrie #339

Merged
merged 9 commits into from
Apr 12, 2017
66 changes: 66 additions & 0 deletions options.go
Original file line number Diff line number Diff line change
Expand Up @@ -178,6 +178,72 @@ type SubmoduleUpdateOptions struct {
RecurseSubmodules SubmoduleRescursivity
}

// CheckoutOptions describes how a checkout 31operation should be performed.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

s/31//

type CheckoutOptions struct {
// Hash to be checked out, if used HEAD will in detached mode. Branch and
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

missing a verb:
if used HEAD will be in detached mode.

// Hash are mutual exclusive.
Hash plumbing.Hash
// Branch to be checked out, if Branch and Hash are empty is set to `master`.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

missing subject:
if Branch and Hash are empty it is set to master.

Branch plumbing.ReferenceName
// Force, if true when switching branches, proceed even if the index or the
// working tree differs from HEAD. This is used to throw away local changes
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

End the sentence with a full stop.

Force bool
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Missing documentation.

}

// Validate validates the fields and sets the default values.
func (o *CheckoutOptions) Validate() error {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why does this method return an error?

On top of that, maybe validate is not the best name for this method, when validating you return true or false according to some checks, while this method overwrites some attributes (and only some of them, leaving other invalid values untouched) probably because there is not constructor or setters to this properly. Maybe a better name will be FillInDefaults.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This was already discussed on previous PRs, and is not related to this PR

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You are right, it was in #178.

I'll open an issue, as we agreed back then.

if o.Branch == "" {
o.Branch = plumbing.Master
}

return nil
}

// ResetMode defines the mode of a reset operation.
type ResetMode int8

const (
// HardReset resets the index and working tree. Any changes to tracked files
// in the working tree are discarded.
HardReset ResetMode = iota
// MixedReset resets the index but not the working tree (i.e., the changed
// files are preserved but not marked for commit) and reports what has not
// been updated. This is the default action.
MixedReset
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can ResetMode be simplified into a boolean for the hard mode?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ResetMode has 5 posible values

// MergeReset resets the index and updates the files in the working tree
// that are different between Commit and HEAD, but keeps those which are
// different between the index and working tree (i.e. which have changes
// which have not been added).
//
// If a file that is different between Commit and the index has unstaged
// changes, reset is aborted.
MergeReset
)

// ResetOptions describes how a reset operation should be performed.
type ResetOptions struct {
// Commit, if commit is pressent set the current branch head (HEAD) to it.
Commit plumbing.Hash
// Mode, form resets the current branch head to Commit and possibly updates
// the index (resetting it to the tree of Commit) and the working tree
// depending on Mode. If empty MixedReset is used.
Mode ResetMode
}

// Validate validates the fields and sets the default values.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The same as before, maybe FillInDefaults is a better name for this function, or just add a constructor that does all the work.

func (o *ResetOptions) Validate(r *Repository) error {
if o.Commit == plumbing.ZeroHash {
ref, err := r.Head()
if err != nil {
return err
}

o.Commit = ref.Hash()
}

return nil
}

// LogOptions describes how a log action should be performed.
type LogOptions struct {
// When the From option is set the log will only contain commits
Expand Down
26 changes: 26 additions & 0 deletions plumbing/format/index/index.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,11 @@ package index

import (
"errors"
"fmt"
"time"

"bytes"

"gopkg.in/src-d/go-git.v4/plumbing"
"gopkg.in/src-d/go-git.v4/plumbing/filemode"
)
Expand Down Expand Up @@ -47,6 +50,16 @@ type Index struct {
ResolveUndo *ResolveUndo
}

// String is equivalent to `git ls-files --stage --debug`
func (i *Index) String() string {
buf := bytes.NewBuffer(nil)
for _, e := range i.Entries {
buf.WriteString(e.String())
}

return buf.String()
}

// Entry represents a single file (or stage of a file) in the cache. An entry
// represents exactly one stage of a file. If a file path is unmerged then
// multiple Entry instances may appear for the same path name.
Expand Down Expand Up @@ -78,6 +91,19 @@ type Entry struct {
IntentToAdd bool
}

func (e Entry) String() string {
buf := bytes.NewBuffer(nil)

fmt.Fprintf(buf, "%06o %s %d\t%s\n", e.Mode, e.Hash, e.Stage, e.Name)
fmt.Fprintf(buf, " ctime: %d:%d\n", e.CreatedAt.Unix(), e.CreatedAt.Nanosecond())
fmt.Fprintf(buf, " mtime: %d:%d\n", e.ModifiedAt.Unix(), e.ModifiedAt.Nanosecond())
fmt.Fprintf(buf, " dev: %d\tino: %d\n", e.Dev, e.Inode)
fmt.Fprintf(buf, " uid: %d\tgid: %d\n", e.UID, e.GID)
fmt.Fprintf(buf, " size: %d\tflags: %x\n", e.Size, 0)

return buf.String()
}

// Tree contains pre-computed hashes for trees that can be derived from the
// index. It helps speed up tree object generation from index for a new commit.
type Tree struct {
Expand Down
4 changes: 2 additions & 2 deletions plumbing/object/difftree.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,8 @@ import (
// DiffTree compares the content and mode of the blobs found via two
// tree objects.
func DiffTree(a, b *Tree) (Changes, error) {
from := newTreeNoder(a)
to := newTreeNoder(b)
from := NewTreeRootNode(a)
to := NewTreeRootNode(b)

hashEqual := func(a, b noder.Hasher) bool {
return bytes.Equal(a.Hash(), b.Hash())
Expand Down
8 changes: 5 additions & 3 deletions plumbing/object/tree.go
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,7 @@ type TreeEntry struct {
// File returns the hash of the file identified by the `path` argument.
// The path is interpreted as relative to the tree receiver.
func (t *Tree) File(path string) (*File, error) {
e, err := t.findEntry(path)
e, err := t.FindEntry(path)
if err != nil {
return nil, ErrFileNotFound
}
Expand All @@ -86,7 +86,7 @@ func (t *Tree) File(path string) (*File, error) {
// Tree returns the tree identified by the `path` argument.
// The path is interpreted as relative to the tree receiver.
func (t *Tree) Tree(path string) (*Tree, error) {
e, err := t.findEntry(path)
e, err := t.FindEntry(path)
if err != nil {
return nil, ErrDirectoryNotFound
}
Expand All @@ -109,7 +109,8 @@ func (t *Tree) TreeEntryFile(e *TreeEntry) (*File, error) {
return NewFile(e.Name, e.Mode, blob), nil
}

func (t *Tree) findEntry(path string) (*TreeEntry, error) {
// FindEntry search a TreeEntry in this tree or any subtree.
func (t *Tree) FindEntry(path string) (*TreeEntry, error) {
pathParts := strings.Split(path, "/")

var tree *Tree
Expand Down Expand Up @@ -146,6 +147,7 @@ func (t *Tree) entry(baseName string) (*TreeEntry, error) {
if t.m == nil {
t.buildMap()
}

entry, ok := t.m[baseName]
if !ok {
return nil, errEntryNotFound
Expand Down
6 changes: 6 additions & 0 deletions plumbing/object/tree_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -107,6 +107,12 @@ func (s *TreeSuite) TestFiles(c *C) {
c.Assert(count, Equals, 9)
}

func (s *TreeSuite) TestFindEntry(c *C) {
e, err := s.Tree.FindEntry("vendor/foo.go")
c.Assert(err, IsNil)
c.Assert(e.Name, Equals, "foo.go")
}

// This plumbing.EncodedObject implementation has a reader that only returns 6
// bytes at a time, this should simulate the conditions when a read
// returns less bytes than asked, for example when reading a hash which
Expand Down
26 changes: 10 additions & 16 deletions plumbing/object/treenoder.go
Original file line number Diff line number Diff line change
@@ -1,13 +1,5 @@
package object

// A treenoder is a helper type that wraps git trees into merkletrie
// noders.
//
// As a merkletrie noder doesn't understand the concept of modes (e.g.
// file permissions), the treenoder includes the mode of the git tree in
// the hash, so changes in the modes will be detected as modifications
// to the file contents by the merkletrie difftree algorithm. This is
// consistent with how the "git diff-tree" command works.
import (
"io"

Expand All @@ -16,6 +8,14 @@ import (
"gopkg.in/src-d/go-git.v4/utils/merkletrie/noder"
)

// A treenoder is a helper type that wraps git trees into merkletrie
// noders.
//
// As a merkletrie noder doesn't understand the concept of modes (e.g.
// file permissions), the treenoder includes the mode of the git tree in
// the hash, so changes in the modes will be detected as modifications
// to the file contents by the merkletrie difftree algorithm. This is
// consistent with how the "git diff-tree" command works.
type treeNoder struct {
parent *Tree // the root node is its own parent
name string // empty string for the root node
Expand All @@ -24,7 +24,8 @@ type treeNoder struct {
children []noder.Noder // memoized
}

func newTreeNoder(t *Tree) *treeNoder {
// NewTreeRootNode returns the root node of a Tree
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Add a full stop at the end of the sentence.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Add a full stop at the end of the sentence.

func NewTreeRootNode(t *Tree) noder.Noder {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think you may have skipped some comments, I copy them here for reference:

This change of name is weird. For once, the type this is constructing is treeNoder, not treeRootNode. Then, a root is always a node, so RootNode is redundant, also my trees are always roots, so TreeRoot is also redundant.

Maybe if you explain what you want to do here we can find a better name for this ctor (and its associated type).

if t == nil {
return &treeNoder{}
}
Expand All @@ -45,13 +46,6 @@ func (t *treeNoder) String() string {
return "treeNoder <" + t.name + ">"
}

// The hash of a treeNoder is the result of concatenating the hash of
// its contents and its mode; that way the difftree algorithm will
// detect changes in the contents of files and also in their mode.
//
// Files with Regular and Deprecated file modes are considered the same
// for the purpose of difftree, so Regular will be used as the mode for
// Deprecated files here.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why do you remove the notice explaining why deprecated files are handled differently than the rest of the files?

func (t *treeNoder) Hash() []byte {
if t.mode == filemode.Deprecated {
return append(t.hash[:], filemode.Regular.Bytes()...)
Expand Down
20 changes: 11 additions & 9 deletions repository.go
Original file line number Diff line number Diff line change
Expand Up @@ -340,11 +340,11 @@ func (r *Repository) clone(o *CloneOptions) error {
return err
}

if _, err := r.updateReferences(c.Fetch, o.ReferenceName, head); err != nil {
if _, err := r.updateReferences(c.Fetch, head); err != nil {
return err
}

if err := r.updateWorktree(); err != nil {
if err := r.updateWorktree(head.Name()); err != nil {
return err
}

Expand Down Expand Up @@ -429,7 +429,7 @@ func (r *Repository) updateRemoteConfig(remote *Remote, o *CloneOptions,
}

func (r *Repository) updateReferences(spec []config.RefSpec,
headName plumbing.ReferenceName, resolvedHead *plumbing.Reference) (updated bool, err error) {
resolvedHead *plumbing.Reference) (updated bool, err error) {

if !resolvedHead.IsBranch() {
// Detached HEAD mode
Expand Down Expand Up @@ -534,7 +534,7 @@ func (r *Repository) Pull(o *PullOptions) error {
return err
}

refsUpdated, err := r.updateReferences(remote.c.Fetch, o.ReferenceName, head)
refsUpdated, err := r.updateReferences(remote.c.Fetch, head)
if err != nil {
return err
}
Expand All @@ -547,7 +547,7 @@ func (r *Repository) Pull(o *PullOptions) error {
return NoErrAlreadyUpToDate
}

if err := r.updateWorktree(); err != nil {
if err := r.updateWorktree(head.Name()); err != nil {
return err
}

Expand All @@ -560,22 +560,24 @@ func (r *Repository) Pull(o *PullOptions) error {
return nil
}

func (r *Repository) updateWorktree() error {
func (r *Repository) updateWorktree(branch plumbing.ReferenceName) error {
if r.wt == nil {
return nil
}

w, err := r.Worktree()
b, err := r.Reference(branch, true)
if err != nil {
return err
}

h, err := r.Head()
w, err := r.Worktree()
if err != nil {
return err
}

return w.Checkout(h.Hash())
return w.Reset(&ResetOptions{
Commit: b.Hash(),
})
}

// Fetch fetches changes from a remote repository.
Expand Down
70 changes: 70 additions & 0 deletions status.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
package git

import "fmt"
import "bytes"

// Status represents the current status of a Worktree.
// The key of the map is the path of the file.
type Status map[string]*FileStatus

// File returns the FileStatus for a given path, if the FileStatus doesn't
// exists a new FileStatus is added to the map using the path as key.
func (s Status) File(path string) *FileStatus {
if _, ok := (s)[path]; !ok {
s[path] = &FileStatus{Worktree: Unmodified, Staging: Unmodified}
}

return s[path]
}

// IsClean returns true if all the files aren't in Unmodified status.
func (s Status) IsClean() bool {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Document the method.

for _, status := range s {
if status.Worktree != Unmodified || status.Staging != Unmodified {
return false
}
}

return true
}

func (s Status) String() string {
buf := bytes.NewBuffer(nil)
for path, status := range s {
if status.Staging == Unmodified && status.Worktree == Unmodified {
continue
}

if status.Staging == Renamed {
path = fmt.Sprintf("%s -> %s", path, status.Extra)
}

fmt.Fprintf(buf, "%c%c %s\n", status.Staging, status.Worktree, path)
}

return buf.String()
}

// FileStatus contains the status of a file in the worktree
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Add a full stop at the end of the comment, and the same for the next 3 comments.

type FileStatus struct {
// Staging is the status of a file in the staging area
Staging StatusCode
// Worktree is the status of a file in the worktree
Worktree StatusCode
// Extra contains extra information, such as the previous name in a rename
Extra string
}

// StatusCode status code of a file in the Worktree
type StatusCode byte

const (
Unmodified StatusCode = ' '
Untracked StatusCode = '?'
Modified StatusCode = 'M'
Added StatusCode = 'A'
Deleted StatusCode = 'D'
Renamed StatusCode = 'R'
Copied StatusCode = 'C'
UpdatedButUnmerged StatusCode = 'U'
)
6 changes: 3 additions & 3 deletions submodule.go
Original file line number Diff line number Diff line change
Expand Up @@ -103,10 +103,10 @@ func (s *Submodule) Update(o *SubmoduleUpdateOptions) error {
return err
}

return s.doRecrusiveUpdate(r, o)
return s.doRecursiveUpdate(r, o)
}

func (s *Submodule) doRecrusiveUpdate(r *Repository, o *SubmoduleUpdateOptions) error {
func (s *Submodule) doRecursiveUpdate(r *Repository, o *SubmoduleUpdateOptions) error {
if o.RecurseSubmodules == NoRecurseSubmodules {
return nil
}
Expand Down Expand Up @@ -140,7 +140,7 @@ func (s *Submodule) fetchAndCheckout(r *Repository, o *SubmoduleUpdateOptions, h
return err
}

if err := w.Checkout(hash); err != nil {
if err := w.Checkout(&CheckoutOptions{Hash: hash}); err != nil {
return err
}

Expand Down
Loading