Merge pull request #985 from mcuadros/checkout-doc

repository: improve CheckoutOption.Hash doc
diff --git a/blame.go b/blame.go
index 349cdd9..adb72d5 100644
--- a/blame.go
+++ b/blame.go
@@ -123,14 +123,25 @@
 }
 
 func newLines(contents []string, commits []*object.Commit) ([]*Line, error) {
-	if len(contents) != len(commits) {
-		return nil, errors.New("contents and commits have different length")
+	lcontents := len(contents)
+	lcommits := len(commits)
+
+	if lcontents != lcommits {
+		if lcontents == lcommits-1 && contents[lcontents-1] != "\n" {
+			contents = append(contents, "\n")
+		} else {
+			return nil, errors.New("contents and commits have different length")
+		}
 	}
-	result := make([]*Line, 0, len(contents))
+
+	result := make([]*Line, 0, lcontents)
 	for i := range contents {
-		l := newLine(commits[i].Author.Email, contents[i], commits[i].Author.When, commits[i].Hash)
-		result = append(result, l)
+		result = append(result, newLine(
+			commits[i].Author.Email, contents[i],
+			commits[i].Author.When, commits[i].Hash,
+		))
 	}
+
 	return result, nil
 }
 
diff --git a/blame_test.go b/blame_test.go
index 92911b1..e0ac129 100644
--- a/blame_test.go
+++ b/blame_test.go
@@ -2,6 +2,7 @@
 
 import (
 	"gopkg.in/src-d/go-git.v4/plumbing"
+	"gopkg.in/src-d/go-git.v4/plumbing/object"
 
 	. "gopkg.in/check.v1"
 	"gopkg.in/src-d/go-git-fixtures.v3"
@@ -13,6 +14,31 @@
 
 var _ = Suite(&BlameSuite{})
 
+func (s *BlameSuite) TestNewLines(c *C) {
+	h := plumbing.NewHash("ce9f123d790717599aaeb76bc62510de437761be")
+	lines, err := newLines([]string{"foo"}, []*object.Commit{{
+		Hash:    h,
+		Message: "foo",
+	}})
+
+	c.Assert(err, IsNil)
+	c.Assert(lines, HasLen, 1)
+	c.Assert(lines[0].Text, Equals, "foo")
+	c.Assert(lines[0].Hash, Equals, h)
+}
+
+func (s *BlameSuite) TestNewLinesWithNewLine(c *C) {
+	lines, err := newLines([]string{"foo"}, []*object.Commit{
+		{Message: "foo"},
+		{Message: "bar"},
+	})
+
+	c.Assert(err, IsNil)
+	c.Assert(lines, HasLen, 2)
+	c.Assert(lines[0].Text, Equals, "foo")
+	c.Assert(lines[1].Text, Equals, "\n")
+}
+
 type blameTest struct {
 	repo   string
 	rev    string
diff --git a/plumbing/format/packfile/packfile.go b/plumbing/format/packfile/packfile.go
index 852a834..0d13066 100644
--- a/plumbing/format/packfile/packfile.go
+++ b/plumbing/format/packfile/packfile.go
@@ -90,6 +90,24 @@
 	return p.nextObject()
 }
 
+// GetSizeByOffset retrieves the size of the encoded object from the
+// packfile with the given offset.
+func (p *Packfile) GetSizeByOffset(o int64) (size int64, err error) {
+	if _, err := p.s.SeekFromStart(o); err != nil {
+		if err == io.EOF || isInvalid(err) {
+			return 0, plumbing.ErrObjectNotFound
+		}
+
+		return 0, err
+	}
+
+	h, err := p.nextObjectHeader()
+	if err != nil {
+		return 0, err
+	}
+	return h.Length, nil
+}
+
 func (p *Packfile) nextObjectHeader() (*ObjectHeader, error) {
 	h, err := p.s.NextObjectHeader()
 	p.s.pendingObject = nil
diff --git a/plumbing/object/tree.go b/plumbing/object/tree.go
index c36a137..78d61a1 100644
--- a/plumbing/object/tree.go
+++ b/plumbing/object/tree.go
@@ -87,6 +87,17 @@
 	return NewFile(path, e.Mode, blob), nil
 }
 
+// Size returns the plaintext size of an object, without reading it
+// into memory.
+func (t *Tree) Size(path string) (int64, error) {
+	e, err := t.FindEntry(path)
+	if err != nil {
+		return 0, ErrEntryNotFound
+	}
+
+	return t.s.EncodedObjectSize(e.Hash)
+}
+
 // 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) {
diff --git a/plumbing/object/tree_test.go b/plumbing/object/tree_test.go
index 7366421..889c63a 100644
--- a/plumbing/object/tree_test.go
+++ b/plumbing/object/tree_test.go
@@ -98,6 +98,12 @@
 	c.Assert(err, Equals, ErrFileNotFound)
 }
 
+func (s *TreeSuite) TestSize(c *C) {
+	size, err := s.Tree.Size("LICENSE")
+	c.Assert(err, IsNil)
+	c.Assert(size, Equals, int64(1072))
+}
+
 func (s *TreeSuite) TestFiles(c *C) {
 	var count int
 	err := s.Tree.Files().ForEach(func(f *File) error {
diff --git a/plumbing/storer/object.go b/plumbing/storer/object.go
index 92aa629..2ac9b09 100644
--- a/plumbing/storer/object.go
+++ b/plumbing/storer/object.go
@@ -40,6 +40,8 @@
 	// HasEncodedObject returns ErrObjNotFound if the object doesn't
 	// exist.  If the object does exist, it returns nil.
 	HasEncodedObject(plumbing.Hash) error
+	// EncodedObjectSize returns the plaintext size of the encoded object.
+	EncodedObjectSize(plumbing.Hash) (int64, error)
 }
 
 // DeltaObjectStorer is an EncodedObjectStorer that can return delta
diff --git a/plumbing/storer/object_test.go b/plumbing/storer/object_test.go
index 6b4fe0f..bc22f7b 100644
--- a/plumbing/storer/object_test.go
+++ b/plumbing/storer/object_test.go
@@ -141,6 +141,16 @@
 	return plumbing.ErrObjectNotFound
 }
 
+func (o *MockObjectStorage) EncodedObjectSize(h plumbing.Hash) (
+	size int64, err error) {
+	for _, o := range o.db {
+		if o.Hash() == h {
+			return o.Size(), nil
+		}
+	}
+	return 0, plumbing.ErrObjectNotFound
+}
+
 func (o *MockObjectStorage) EncodedObject(t plumbing.ObjectType, h plumbing.Hash) (plumbing.EncodedObject, error) {
 	for _, o := range o.db {
 		if o.Hash() == h {
diff --git a/repository.go b/repository.go
index ddf6727..507ff44 100644
--- a/repository.go
+++ b/repository.go
@@ -175,15 +175,6 @@
 		return nil, err
 	}
 
-	cfg, err := s.Config()
-	if err != nil {
-		return nil, err
-	}
-
-	if !cfg.Core.IsBare && worktree == nil {
-		return nil, ErrWorktreeNotProvided
-	}
-
 	return newRepository(s, worktree), nil
 }
 
@@ -335,6 +326,8 @@
 // PlainClone a repository into the path with the given options, isBare defines
 // if the new repository will be bare or normal. If the path is not empty
 // ErrRepositoryAlreadyExists is returned.
+//
+// TODO(mcuadros): move isBare to CloneOptions in v5
 func PlainClone(path string, isBare bool, o *CloneOptions) (*Repository, error) {
 	return PlainCloneContext(context.Background(), path, isBare, o)
 }
@@ -346,6 +339,8 @@
 // The provided Context must be non-nil. If the context expires before the
 // operation is complete, an error is returned. The context only affects to the
 // transport operations.
+//
+// TODO(mcuadros): move isBare to CloneOptions in v5
 func PlainCloneContext(ctx context.Context, path string, isBare bool, o *CloneOptions) (*Repository, error) {
 	r, err := PlainInit(path, isBare)
 	if err != nil {
@@ -1171,7 +1166,18 @@
 	return &Worktree{r: r, Filesystem: r.wt}, nil
 }
 
-// ResolveRevision resolves revision to corresponding hash.
+func countTrue(vals ...bool) int {
+	sum := 0
+	for _, v := range vals {
+		if v {
+			sum++
+		}
+	}
+	return sum
+}
+
+// ResolveRevision resolves revision to corresponding hash. It will always
+// resolve to a commit hash, not a tree or annotated tag.
 //
 // Implemented resolvers : HEAD, branch, tag, heads/branch, refs/heads/branch,
 // refs/tags/tag, refs/remotes/origin/branch, refs/remotes/origin/HEAD, tilde and caret (HEAD~1, master~^, tag~2, ref/heads/master~1, ...), selection by text (HEAD^{/fix nasty bug})
@@ -1191,8 +1197,8 @@
 		case revision.Ref:
 			revisionRef := item.(revision.Ref)
 			var ref *plumbing.Reference
-			var hashCommit, refCommit *object.Commit
-			var rErr, hErr error
+			var hashCommit, refCommit, tagCommit *object.Commit
+			var rErr, hErr, tErr error
 
 			for _, rule := range append([]string{"%s"}, plumbing.RefRevParseRules...) {
 				ref, err = storer.ResolveReference(r.Storer, plumbing.ReferenceName(fmt.Sprintf(rule, revisionRef)))
@@ -1203,24 +1209,38 @@
 			}
 
 			if ref != nil {
+				tag, tObjErr := r.TagObject(ref.Hash())
+				if tObjErr != nil {
+					tErr = tObjErr
+				} else {
+					tagCommit, tErr = tag.Commit()
+				}
 				refCommit, rErr = r.CommitObject(ref.Hash())
 			} else {
 				rErr = plumbing.ErrReferenceNotFound
+				tErr = plumbing.ErrReferenceNotFound
 			}
 
-			isHash := plumbing.NewHash(string(revisionRef)).String() == string(revisionRef)
-
-			if isHash {
+			maybeHash := plumbing.NewHash(string(revisionRef)).String() == string(revisionRef)
+			if maybeHash {
 				hashCommit, hErr = r.CommitObject(plumbing.NewHash(string(revisionRef)))
+			} else {
+				hErr = plumbing.ErrReferenceNotFound
 			}
 
+			isTag := tErr == nil
+			isCommit := rErr == nil
+			isHash := hErr == nil
+
 			switch {
-			case rErr == nil && !isHash:
-				commit = refCommit
-			case rErr != nil && isHash && hErr == nil:
-				commit = hashCommit
-			case rErr == nil && isHash && hErr == nil:
+			case countTrue(isTag, isCommit, isHash) > 1:
 				return &plumbing.ZeroHash, fmt.Errorf(`refname "%s" is ambiguous`, revisionRef)
+			case isTag:
+				commit = tagCommit
+			case isCommit:
+				commit = refCommit
+			case isHash:
+				commit = hashCommit
 			default:
 				return &plumbing.ZeroHash, plumbing.ErrReferenceNotFound
 			}
diff --git a/repository_test.go b/repository_test.go
index 6d34d42..07c3570 100644
--- a/repository_test.go
+++ b/repository_test.go
@@ -143,7 +143,7 @@
 	c.Assert(r, NotNil)
 }
 
-func (s *RepositorySuite) TestOpenMissingWorktree(c *C) {
+func (s *RepositorySuite) TestOpenBareMissingWorktree(c *C) {
 	st := memory.NewStorage()
 
 	r, err := Init(st, memfs.New())
@@ -151,8 +151,8 @@
 	c.Assert(r, NotNil)
 
 	r, err = Open(st, nil)
-	c.Assert(err, Equals, ErrWorktreeNotProvided)
-	c.Assert(r, IsNil)
+	c.Assert(err, IsNil)
+	c.Assert(r, NotNil)
 }
 
 func (s *RepositorySuite) TestOpenNotExists(c *C) {
@@ -425,8 +425,8 @@
 	c.Assert(r, NotNil)
 
 	r, err = PlainOpen(filepath.Join(dir, ".git"))
-	c.Assert(err, Equals, ErrWorktreeNotProvided)
-	c.Assert(r, IsNil)
+	c.Assert(err, IsNil)
+	c.Assert(r, NotNil)
 }
 
 func (s *RepositorySuite) testPlainOpenGitFile(c *C, f func(string, string) string) {
@@ -2065,7 +2065,25 @@
 		h, err := r.ResolveRevision(plumbing.Revision(rev))
 
 		c.Assert(err, IsNil)
-		c.Assert(h.String(), Equals, hash)
+		c.Check(h.String(), Equals, hash, Commentf("while checking %s", rev))
+	}
+}
+
+func (s *RepositorySuite) TestResolveRevisionAnnotated(c *C) {
+	f := fixtures.ByURL("https://github.com/git-fixtures/tags.git").One()
+	sto := filesystem.NewStorage(f.DotGit(), cache.NewObjectLRUDefault())
+	r, err := Open(sto, f.DotGit())
+	c.Assert(err, IsNil)
+
+	datas := map[string]string{
+		"refs/tags/annotated-tag": "f7b877701fbf855b44c0a9e86f3fdce2c298b07f",
+	}
+
+	for rev, hash := range datas {
+		h, err := r.ResolveRevision(plumbing.Revision(rev))
+
+		c.Assert(err, IsNil)
+		c.Check(h.String(), Equals, hash, Commentf("while checking %s", rev))
 	}
 }
 
diff --git a/storage/filesystem/object.go b/storage/filesystem/object.go
index 68bd140..6cd2d4c 100644
--- a/storage/filesystem/object.go
+++ b/storage/filesystem/object.go
@@ -160,6 +160,79 @@
 	return nil
 }
 
+func (s *ObjectStorage) encodedObjectSizeFromUnpacked(h plumbing.Hash) (
+	size int64, err error) {
+	f, err := s.dir.Object(h)
+	if err != nil {
+		if os.IsNotExist(err) {
+			return 0, plumbing.ErrObjectNotFound
+		}
+
+		return 0, err
+	}
+
+	r, err := objfile.NewReader(f)
+	if err != nil {
+		return 0, err
+	}
+	defer ioutil.CheckClose(r, &err)
+
+	_, size, err = r.Header()
+	return size, err
+}
+
+func (s *ObjectStorage) encodedObjectSizeFromPackfile(h plumbing.Hash) (
+	size int64, err error) {
+	if err := s.requireIndex(); err != nil {
+		return 0, err
+	}
+
+	pack, _, offset := s.findObjectInPackfile(h)
+	if offset == -1 {
+		return 0, plumbing.ErrObjectNotFound
+	}
+
+	f, err := s.dir.ObjectPack(pack)
+	if err != nil {
+		return 0, err
+	}
+	defer ioutil.CheckClose(f, &err)
+
+	idx := s.index[pack]
+	hash, err := idx.FindHash(offset)
+	if err == nil {
+		obj, ok := s.deltaBaseCache.Get(hash)
+		if ok {
+			return obj.Size(), nil
+		}
+	} else if err != nil && err != plumbing.ErrObjectNotFound {
+		return 0, err
+	}
+
+	var p *packfile.Packfile
+	if s.deltaBaseCache != nil {
+		p = packfile.NewPackfileWithCache(idx, s.dir.Fs(), f, s.deltaBaseCache)
+	} else {
+		p = packfile.NewPackfile(idx, s.dir.Fs(), f)
+	}
+
+	return p.GetSizeByOffset(offset)
+}
+
+// EncodedObjectSize returns the plaintext size of the given object,
+// without actually reading the full object data from storage.
+func (s *ObjectStorage) EncodedObjectSize(h plumbing.Hash) (
+	size int64, err error) {
+	size, err = s.encodedObjectSizeFromUnpacked(h)
+	if err != nil && err != plumbing.ErrObjectNotFound {
+		return 0, err
+	} else if err == nil {
+		return size, nil
+	}
+
+	return s.encodedObjectSizeFromPackfile(h)
+}
+
 // EncodedObject returns the object with the given hash, by searching for it in
 // the packfile and the git object directories.
 func (s *ObjectStorage) EncodedObject(t plumbing.ObjectType, h plumbing.Hash) (plumbing.EncodedObject, error) {
diff --git a/storage/filesystem/object_test.go b/storage/filesystem/object_test.go
index 407abf2..4e6bbfb 100644
--- a/storage/filesystem/object_test.go
+++ b/storage/filesystem/object_test.go
@@ -83,6 +83,44 @@
 	})
 }
 
+func (s *FsSuite) TestGetSizeOfObjectFile(c *C) {
+	fs := fixtures.ByTag(".git").ByTag("unpacked").One().DotGit()
+	o := NewObjectStorage(dotgit.New(fs), cache.NewObjectLRUDefault())
+
+	// Get the size of `tree_walker.go`.
+	expected := plumbing.NewHash("cbd81c47be12341eb1185b379d1c82675aeded6a")
+	size, err := o.EncodedObjectSize(expected)
+	c.Assert(err, IsNil)
+	c.Assert(size, Equals, int64(2412))
+}
+
+func (s *FsSuite) TestGetSizeFromPackfile(c *C) {
+	fixtures.Basic().ByTag(".git").Test(c, func(f *fixtures.Fixture) {
+		fs := f.DotGit()
+		o := NewObjectStorage(dotgit.New(fs), cache.NewObjectLRUDefault())
+
+		// Get the size of `binary.jpg`.
+		expected := plumbing.NewHash("d5c0f4ab811897cadf03aec358ae60d21f91c50d")
+		size, err := o.EncodedObjectSize(expected)
+		c.Assert(err, IsNil)
+		c.Assert(size, Equals, int64(76110))
+	})
+}
+
+func (s *FsSuite) TestGetSizeOfAllObjectFiles(c *C) {
+	fs := fixtures.ByTag(".git").One().DotGit()
+	o := NewObjectStorage(dotgit.New(fs), cache.NewObjectLRUDefault())
+
+	// Get the size of `tree_walker.go`.
+	err := o.ForEachObjectHash(func(h plumbing.Hash) error {
+		size, err := o.EncodedObjectSize(h)
+		c.Assert(err, IsNil)
+		c.Assert(size, Not(Equals), int64(0))
+		return nil
+	})
+	c.Assert(err, IsNil)
+}
+
 func (s *FsSuite) TestGetFromPackfileMultiplePackfiles(c *C) {
 	fs := fixtures.ByTag(".git").ByTag("multi-packfile").One().DotGit()
 	o := NewObjectStorage(dotgit.New(fs), cache.NewObjectLRUDefault())
diff --git a/storage/memory/storage.go b/storage/memory/storage.go
index 2e32509..6e11742 100644
--- a/storage/memory/storage.go
+++ b/storage/memory/storage.go
@@ -122,6 +122,16 @@
 	return nil
 }
 
+func (o *ObjectStorage) EncodedObjectSize(h plumbing.Hash) (
+	size int64, err error) {
+	obj, ok := o.Objects[h]
+	if !ok {
+		return 0, plumbing.ErrObjectNotFound
+	}
+
+	return obj.Size(), nil
+}
+
 func (o *ObjectStorage) EncodedObject(t plumbing.ObjectType, h plumbing.Hash) (plumbing.EncodedObject, error) {
 	obj, ok := o.Objects[h]
 	if !ok || (plumbing.AnyObject != t && obj.Type() != t) {