Merge pull request #1165 from seletskiy/push-prune

Remote: add Prune option to PushOptions
diff --git a/COMPATIBILITY.md b/COMPATIBILITY.md
index e07e799..4a3da62 100644
--- a/COMPATIBILITY.md
+++ b/COMPATIBILITY.md
@@ -86,7 +86,7 @@
 | for-each-ref                          | ✔ |
 | hash-object                           | ✔ |
 | ls-files                              | ✔ |
-| merge-base                            | |
+| merge-base                            | ✔ | Calculates the merge-base only between two commits, and supports `--independent` and `--is-ancestor` modifiers; Does not support `--fork-point` nor `--octopus` modifiers. |
 | read-tree                             | |
 | rev-list                              | ✔ |
 | rev-parse                             | |
diff --git a/_examples/common_test.go b/_examples/common_test.go
index 47463a1..89d49a3 100644
--- a/_examples/common_test.go
+++ b/_examples/common_test.go
@@ -29,6 +29,7 @@
 	"tag":         {cloneRepository(defaultURL, tempFolder())},
 	"pull":        {createRepositoryWithRemote(tempFolder(), defaultURL)},
 	"ls":          {cloneRepository(defaultURL, tempFolder()), "HEAD", "vendor"},
+	"merge_base":  {cloneRepository(defaultURL, tempFolder()), "--is-ancestor", "HEAD~3", "HEAD^"},
 }
 
 var ignored = map[string]bool{}
@@ -50,14 +51,15 @@
 	}
 
 	for _, example := range examples {
-		_, name := filepath.Split(filepath.Dir(example))
+		dir := filepath.Dir(example)
+		_, name := filepath.Split(dir)
 
 		if ignored[name] {
 			continue
 		}
 
 		t.Run(name, func(t *testing.T) {
-			testExample(t, name, example)
+			testExample(t, name, dir)
 		})
 	}
 }
@@ -135,10 +137,9 @@
 	CheckIfError(err)
 }
 
-func testExample(t *testing.T, name, example string) {
-	cmd := exec.Command("go", append([]string{
-		"run", filepath.Join(example),
-	}, args[name]...)...)
+func testExample(t *testing.T, name, dir string) {
+	arguments := append([]string{"run", dir}, args[name]...)
+	cmd := exec.Command("go", arguments...)
 
 	cmd.Stdout = os.Stdout
 	cmd.Stderr = os.Stderr
diff --git a/_examples/ls-remote/main.go b/_examples/ls-remote/main.go
new file mode 100644
index 0000000..68c0454
--- /dev/null
+++ b/_examples/ls-remote/main.go
@@ -0,0 +1,42 @@
+package main
+
+import (
+	"log"
+
+	"gopkg.in/src-d/go-git.v4"
+	"gopkg.in/src-d/go-git.v4/config"
+	"gopkg.in/src-d/go-git.v4/storage/memory"
+)
+
+// Retrieve remote tags without cloning repository
+func main() {
+
+	// Create the remote with repository URL
+	rem := git.NewRemote(memory.NewStorage(), &config.RemoteConfig{
+		Name: "origin",
+		URLs: []string{"https://github.com/Zenika/MARCEL"},
+	})
+
+	log.Print("Fetching tags...")
+
+	// We can then use every Remote functions to retrieve wanted informations
+	refs, err := rem.List(&git.ListOptions{})
+	if err != nil {
+		log.Fatal(err)
+	}
+
+	// Filters the references list and only keeps tags
+	var tags []string
+	for _, ref := range refs {
+		if ref.Name().IsTag() {
+			tags = append(tags, ref.Name().Short())
+		}
+	}
+
+	if len(tags) == 0 {
+		log.Println("No tags!")
+		return
+	}
+
+	log.Printf("Tags found: %v", tags)
+}
diff --git a/_examples/merge_base/help-long.msg.go b/_examples/merge_base/help-long.msg.go
new file mode 100644
index 0000000..7759cbd
--- /dev/null
+++ b/_examples/merge_base/help-long.msg.go
@@ -0,0 +1,63 @@
+package main
+
+const helpLongMsg = `
+NAME:
+   %_COMMAND_NAME_% - Lists the best common ancestors of the two passed commit revisions
+
+SYNOPSIS:
+  usage: %_COMMAND_NAME_% <path> <commitRev> <commitRev>
+     or: %_COMMAND_NAME_% <path> --independent <commitRev>...
+     or: %_COMMAND_NAME_% <path> --is-ancestor <commitRev> <commitRev>
+
+ params:
+    <path>       Path to the git repository
+    <commitRev>  Git revision as supported by go-git
+
+DESCRIPTION:
+    %_COMMAND_NAME_% finds the best common ancestor(s) between two commits. One common ancestor is better than another common ancestor if the latter is an ancestor of the former.
+    A common ancestor that does not have any better common ancestor is a best common ancestor, i.e. a merge base. Note that there can be more than one merge base for a pair of commits.
+    Commits that does not share a common history has no common ancestors.
+
+OPTIONS:
+    As the most common special case, specifying only two commits on the command line means computing the merge base between the given two commits.
+    If there is no shared history between the passed commits, there won't be a merge-base, and the command will exit with status 1.
+
+--independent
+    List the subgroup from the passed commits, that cannot be reached from any other of the passed ones. In other words, it prints a minimal subset of the supplied commits with the same ancestors.
+
+--is-ancestor
+    Check if the first commit is an ancestor of the second one, and exit with status 0 if true, or with status 1 if not. Errors are signaled by a non-zero status that is not 1.
+
+DISCUSSION:
+    Given two commits A and B, %_COMMAND_NAME_% A B will output a commit which is the best common ancestor of both, what means that is reachable from both A and B through the parent relationship.
+
+    For example, with this topology:
+
+             o---o---o---o---B
+            /       /
+    ---3---2---o---1---o---A
+
+    the merge base between A and B is 1.
+
+    With the given topology 2 and 3 are also common ancestors of A and B, but they are not the best ones because they can be also reached from 1.
+
+    When the history involves cross-cross merges, there can be more than one best common ancestor for two commits. For example, with this topology:
+
+    ---1---o---A
+        \ /
+         X
+        / \
+    ---2---o---o---B
+
+    When the history involves feature branches depending on other feature branches there can be also more than one common ancestor. For example:
+
+
+           o---o---o
+          /         \
+         1---o---A   \
+        /       /     \
+    ---o---o---2---o---o---B
+
+    In both examples, both 1 and 2 are merge-bases of A and B for each situation.
+    Neither one is better than the other (both are best merge bases) because 1 cannot be reached from 2, nor the opposite.
+`
diff --git a/_examples/merge_base/helpers.go b/_examples/merge_base/helpers.go
new file mode 100644
index 0000000..b7b1ed6
--- /dev/null
+++ b/_examples/merge_base/helpers.go
@@ -0,0 +1,61 @@
+package main
+
+import (
+	"fmt"
+	"os"
+	"strings"
+
+	"gopkg.in/src-d/go-git.v4/plumbing/object"
+)
+
+func checkIfError(err error, code exitCode, mainReason string, v ...interface{}) {
+	if err == nil {
+		return
+	}
+
+	printErr(wrappErr(err, mainReason, v...))
+	os.Exit(int(code))
+}
+
+func helpAndExit(s string, helpMsg string, code exitCode) {
+	if code == exitCodeSuccess {
+		printMsg("%s", s)
+	} else {
+		printErr(fmt.Errorf(s))
+	}
+
+	fmt.Println(strings.Replace(helpMsg, "%_COMMAND_NAME_%", os.Args[0], -1))
+
+	os.Exit(int(code))
+}
+
+func printErr(err error) {
+	fmt.Printf("\x1b[31;1m%s\x1b[0m\n", fmt.Sprintf("error: %s", err))
+}
+
+func printMsg(format string, args ...interface{}) {
+	fmt.Printf("%s\n", fmt.Sprintf(format, args...))
+}
+
+func printCommits(commits []*object.Commit) {
+	for _, commit := range commits {
+		if os.Getenv("LOG_LEVEL") == "verbose" {
+			fmt.Printf(
+				"\x1b[36;1m%s \x1b[90;21m%s\x1b[0m %s\n",
+				commit.Hash.String()[:7],
+				commit.Hash.String(),
+				strings.Split(commit.Message, "\n")[0],
+			)
+		} else {
+			fmt.Println(commit.Hash.String())
+		}
+	}
+}
+
+func wrappErr(err error, s string, v ...interface{}) error {
+	if err != nil {
+		return fmt.Errorf("%s\n  %s", fmt.Sprintf(s, v...), err)
+	}
+
+	return nil
+}
diff --git a/_examples/merge_base/main.go b/_examples/merge_base/main.go
new file mode 100644
index 0000000..fe6abc6
--- /dev/null
+++ b/_examples/merge_base/main.go
@@ -0,0 +1,124 @@
+package main
+
+import (
+	"os"
+
+	"gopkg.in/src-d/go-git.v4"
+	"gopkg.in/src-d/go-git.v4/plumbing"
+	"gopkg.in/src-d/go-git.v4/plumbing/object"
+)
+
+type exitCode int
+
+const (
+	exitCodeSuccess exitCode = iota
+	exitCodeNotFound
+	exitCodeWrongSyntax
+	exitCodeCouldNotOpenRepository
+	exitCodeCouldNotParseRevision
+	exitCodeUnexpected
+
+	cmdDesc = "Returns the merge-base between two commits:"
+
+	helpShortMsg = `
+  usage: %_COMMAND_NAME_% <path> <commitRev> <commitRev>
+     or: %_COMMAND_NAME_% <path> --independent <commitRev>...
+     or: %_COMMAND_NAME_% <path> --is-ancestor <commitRev> <commitRev>
+     or: %_COMMAND_NAME_% --help
+
+ params:
+    <path>          path to the git repository
+    <commitRev>     git revision as supported by go-git
+
+options:
+    (no options)    lists the best common ancestors of the two passed commits
+    --independent   list commits not reachable from the others
+    --is-ancestor   is the first one ancestor of the other?
+    --help          show the full help message of %_COMMAND_NAME_%
+`
+)
+
+// Command that mimics `git merge-base --all <baseRev> <headRev>`
+// Command that mimics `git merge-base --is-ancestor <baseRev> <headRev>`
+// Command that mimics `git merge-base --independent <commitRev>...`
+func main() {
+	if len(os.Args) == 1 {
+		helpAndExit("Returns the merge-base between two commits:", helpShortMsg, exitCodeSuccess)
+	}
+
+	if os.Args[1] == "--help" || os.Args[1] == "-h" {
+		helpAndExit("Returns the merge-base between two commits:", helpLongMsg, exitCodeSuccess)
+	}
+
+	if len(os.Args) < 4 {
+		helpAndExit("Wrong syntax", helpShortMsg, exitCodeWrongSyntax)
+	}
+
+	path := os.Args[1]
+
+	var modeIndependent, modeAncestor bool
+	var commitRevs []string
+	var res []*object.Commit
+
+	switch os.Args[2] {
+	case "--independent":
+		modeIndependent = true
+		commitRevs = os.Args[3:]
+	case "--is-ancestor":
+		modeAncestor = true
+		commitRevs = os.Args[3:]
+		if len(commitRevs) != 2 {
+			helpAndExit("Wrong syntax", helpShortMsg, exitCodeWrongSyntax)
+		}
+	default:
+		commitRevs = os.Args[2:]
+		if len(commitRevs) != 2 {
+			helpAndExit("Wrong syntax", helpShortMsg, exitCodeWrongSyntax)
+		}
+	}
+
+	// Open a git repository from current directory
+	repo, err := git.PlainOpen(path)
+	checkIfError(err, exitCodeCouldNotOpenRepository, "not in a git repository")
+
+	// Get the hashes of the passed revisions
+	var hashes []*plumbing.Hash
+	for _, rev := range commitRevs {
+		hash, err := repo.ResolveRevision(plumbing.Revision(rev))
+		checkIfError(err, exitCodeCouldNotParseRevision, "could not parse revision '%s'", rev)
+		hashes = append(hashes, hash)
+	}
+
+	// Get the commits identified by the passed hashes
+	var commits []*object.Commit
+	for _, hash := range hashes {
+		commit, err := repo.CommitObject(*hash)
+		checkIfError(err, exitCodeUnexpected, "could not find commit '%s'", hash.String())
+		commits = append(commits, commit)
+	}
+
+	if modeAncestor {
+		isAncestor, err := commits[0].IsAncestor(commits[1])
+		checkIfError(err, exitCodeUnexpected, "could not traverse the repository history")
+
+		if !isAncestor {
+			os.Exit(int(exitCodeNotFound))
+		}
+
+		os.Exit(int(exitCodeSuccess))
+	}
+
+	if modeIndependent {
+		res, err = object.Independents(commits)
+		checkIfError(err, exitCodeUnexpected, "could not traverse the repository history")
+	} else {
+		res, err = commits[0].MergeBase(commits[1])
+		checkIfError(err, exitCodeUnexpected, "could not traverse the repository history")
+
+		if len(res) == 0 {
+			os.Exit(int(exitCodeNotFound))
+		}
+	}
+
+	printCommits(res)
+}
diff --git a/config/refspec.go b/config/refspec.go
index b2b3203..14bb400 100644
--- a/config/refspec.go
+++ b/config/refspec.go
@@ -18,7 +18,7 @@
 	ErrRefSpecMalformedWildcard  = errors.New("malformed refspec, mismatched number of wildcards")
 )
 
-// RefSpec is a mapping from local branches to remote references
+// RefSpec is a mapping from local branches to remote references.
 // The format of the refspec is an optional +, followed by <src>:<dst>, where
 // <src> is the pattern for references on the remote side and <dst> is where
 // those references will be written locally. The + tells Git to update the
@@ -99,11 +99,11 @@
 
 	var prefix, suffix string
 	prefix = src[0:wildcard]
-	if len(src) < wildcard {
-		suffix = src[wildcard+1 : len(suffix)]
+	if len(src) > wildcard+1 {
+		suffix = src[wildcard+1:]
 	}
 
-	return len(name) > len(prefix)+len(suffix) &&
+	return len(name) >= len(prefix)+len(suffix) &&
 		strings.HasPrefix(name, prefix) &&
 		strings.HasSuffix(name, suffix)
 }
diff --git a/config/refspec_test.go b/config/refspec_test.go
index b925cba..aaeac73 100644
--- a/config/refspec_test.go
+++ b/config/refspec_test.go
@@ -96,9 +96,38 @@
 }
 
 func (s *RefSpecSuite) TestRefSpecMatchGlob(c *C) {
-	spec := RefSpec("refs/heads/*:refs/remotes/origin/*")
-	c.Assert(spec.Match(plumbing.ReferenceName("refs/tag/foo")), Equals, false)
-	c.Assert(spec.Match(plumbing.ReferenceName("refs/heads/foo")), Equals, true)
+	tests := map[string]map[string]bool{
+		"refs/heads/*:refs/remotes/origin/*": {
+			"refs/tag/foo":   false,
+			"refs/heads/foo": true,
+		},
+		"refs/heads/*bc:refs/remotes/origin/*bc": {
+			"refs/heads/abc": true,
+			"refs/heads/bc":  true,
+			"refs/heads/abx": false,
+		},
+		"refs/heads/a*c:refs/remotes/origin/a*c": {
+			"refs/heads/abc": true,
+			"refs/heads/ac":  true,
+			"refs/heads/abx": false,
+		},
+		"refs/heads/ab*:refs/remotes/origin/ab*": {
+			"refs/heads/abc": true,
+			"refs/heads/ab":  true,
+			"refs/heads/xbc": false,
+		},
+	}
+
+	for specStr, data := range tests {
+		spec := RefSpec(specStr)
+		for ref, matches := range data {
+			c.Assert(spec.Match(plumbing.ReferenceName(ref)),
+				Equals,
+				matches,
+				Commentf("while matching spec %q against ref %q", specStr, ref),
+			)
+		}
+	}
 }
 
 func (s *RefSpecSuite) TestRefSpecDst(c *C) {
@@ -110,11 +139,33 @@
 }
 
 func (s *RefSpecSuite) TestRefSpecDstBlob(c *C) {
-	spec := RefSpec("refs/heads/*:refs/remotes/origin/*")
-	c.Assert(
-		spec.Dst(plumbing.ReferenceName("refs/heads/foo")).String(), Equals,
-		"refs/remotes/origin/foo",
-	)
+	ref := "refs/heads/abc"
+	tests := map[string]string{
+		"refs/heads/*:refs/remotes/origin/*":       "refs/remotes/origin/abc",
+		"refs/heads/*bc:refs/remotes/origin/*":     "refs/remotes/origin/a",
+		"refs/heads/*bc:refs/remotes/origin/*bc":   "refs/remotes/origin/abc",
+		"refs/heads/a*c:refs/remotes/origin/*":     "refs/remotes/origin/b",
+		"refs/heads/a*c:refs/remotes/origin/a*c":   "refs/remotes/origin/abc",
+		"refs/heads/ab*:refs/remotes/origin/*":     "refs/remotes/origin/c",
+		"refs/heads/ab*:refs/remotes/origin/ab*":   "refs/remotes/origin/abc",
+		"refs/heads/*abc:refs/remotes/origin/*abc": "refs/remotes/origin/abc",
+		"refs/heads/abc*:refs/remotes/origin/abc*": "refs/remotes/origin/abc",
+		// for these two cases, git specifically logs:
+		// error: * Ignoring funny ref 'refs/remotes/origin/' locally
+		// and ignores the ref; go-git does not currently do this validation,
+		// but probably should.
+		// "refs/heads/*abc:refs/remotes/origin/*": "",
+		// "refs/heads/abc*:refs/remotes/origin/*": "",
+	}
+
+	for specStr, dst := range tests {
+		spec := RefSpec(specStr)
+		c.Assert(spec.Dst(plumbing.ReferenceName(ref)).String(),
+			Equals,
+			dst,
+			Commentf("while getting dst from spec %q with ref %q", specStr, ref),
+		)
+	}
 }
 
 func (s *RefSpecSuite) TestRefSpecReverse(c *C) {
diff --git a/plumbing/format/index/doc.go b/plumbing/format/index/doc.go
index f2b3d76..39ae6ad 100644
--- a/plumbing/format/index/doc.go
+++ b/plumbing/format/index/doc.go
@@ -320,7 +320,7 @@
 //  == End of Index Entry
 //
 //    The End of Index Entry (EOIE) is used to locate the end of the variable
-//    length index entries and the begining of the extensions. Code can take
+//    length index entries and the beginning of the extensions. Code can take
 //    advantage of this to quickly locate the index extensions without having
 //    to parse through all of the index entries.
 //
@@ -353,7 +353,7 @@
 //
 //    - A number of index offset entries each consisting of:
 //
-//    - 32-bit offset from the begining of the file to the first cache entry
+//    - 32-bit offset from the beginning of the file to the first cache entry
 //      in this block of entries.
 //
 //    - 32-bit count of cache entries in this blockpackage index
diff --git a/plumbing/format/index/index.go b/plumbing/format/index/index.go
index 6c4b7ca..6653c91 100644
--- a/plumbing/format/index/index.go
+++ b/plumbing/format/index/index.go
@@ -198,7 +198,7 @@
 }
 
 // EndOfIndexEntry is the End of Index Entry (EOIE) is used to locate the end of
-// the variable length index entries and the begining of the extensions. Code
+// the variable length index entries and the beginning of the extensions. Code
 // can take advantage of this to quickly locate the index extensions without
 // having to parse through all of the index entries.
 //
diff --git a/remote.go b/remote.go
index 3ed2d0a..e229ef1 100644
--- a/remote.go
+++ b/remote.go
@@ -45,7 +45,10 @@
 	s storage.Storer
 }
 
-func newRemote(s storage.Storer, c *config.RemoteConfig) *Remote {
+// NewRemote creates a new Remote.
+// The intended purpose is to use the Remote for tasks such as listing remote references (like using git ls-remote).
+// Otherwise Remotes should be created via the use of a Repository.
+func NewRemote(s storage.Storer, c *config.RemoteConfig) *Remote {
 	return &Remote{s: s, c: c}
 }
 
diff --git a/remote_test.go b/remote_test.go
index 290b574..a45d814 100644
--- a/remote_test.go
+++ b/remote_test.go
@@ -31,32 +31,32 @@
 var _ = Suite(&RemoteSuite{})
 
 func (s *RemoteSuite) TestFetchInvalidEndpoint(c *C) {
-	r := newRemote(nil, &config.RemoteConfig{Name: "foo", URLs: []string{"http://\\"}})
+	r := NewRemote(nil, &config.RemoteConfig{Name: "foo", URLs: []string{"http://\\"}})
 	err := r.Fetch(&FetchOptions{RemoteName: "foo"})
 	c.Assert(err, ErrorMatches, ".*invalid character.*")
 }
 
 func (s *RemoteSuite) TestFetchNonExistentEndpoint(c *C) {
-	r := newRemote(nil, &config.RemoteConfig{Name: "foo", URLs: []string{"ssh://non-existent/foo.git"}})
+	r := NewRemote(nil, &config.RemoteConfig{Name: "foo", URLs: []string{"ssh://non-existent/foo.git"}})
 	err := r.Fetch(&FetchOptions{})
 	c.Assert(err, NotNil)
 }
 
 func (s *RemoteSuite) TestFetchInvalidSchemaEndpoint(c *C) {
-	r := newRemote(nil, &config.RemoteConfig{Name: "foo", URLs: []string{"qux://foo"}})
+	r := NewRemote(nil, &config.RemoteConfig{Name: "foo", URLs: []string{"qux://foo"}})
 	err := r.Fetch(&FetchOptions{})
 	c.Assert(err, ErrorMatches, ".*unsupported scheme.*")
 }
 
 func (s *RemoteSuite) TestFetchInvalidFetchOptions(c *C) {
-	r := newRemote(nil, &config.RemoteConfig{Name: "foo", URLs: []string{"qux://foo"}})
+	r := NewRemote(nil, &config.RemoteConfig{Name: "foo", URLs: []string{"qux://foo"}})
 	invalid := config.RefSpec("^*$ñ")
 	err := r.Fetch(&FetchOptions{RefSpecs: []config.RefSpec{invalid}})
 	c.Assert(err, Equals, config.ErrRefSpecMalformedSeparator)
 }
 
 func (s *RemoteSuite) TestFetchWildcard(c *C) {
-	r := newRemote(memory.NewStorage(), &config.RemoteConfig{
+	r := NewRemote(memory.NewStorage(), &config.RemoteConfig{
 		URLs: []string{s.GetBasicLocalRepositoryURL()},
 	})
 
@@ -72,7 +72,7 @@
 }
 
 func (s *RemoteSuite) TestFetchWildcardTags(c *C) {
-	r := newRemote(memory.NewStorage(), &config.RemoteConfig{
+	r := NewRemote(memory.NewStorage(), &config.RemoteConfig{
 		URLs: []string{s.GetLocalRepositoryURL(fixtures.ByTag("tags").One())},
 	})
 
@@ -91,7 +91,7 @@
 }
 
 func (s *RemoteSuite) TestFetch(c *C) {
-	r := newRemote(memory.NewStorage(), &config.RemoteConfig{
+	r := NewRemote(memory.NewStorage(), &config.RemoteConfig{
 		URLs: []string{s.GetLocalRepositoryURL(fixtures.ByTag("tags").One())},
 	})
 
@@ -105,7 +105,7 @@
 }
 
 func (s *RemoteSuite) TestFetchNonExistantReference(c *C) {
-	r := newRemote(memory.NewStorage(), &config.RemoteConfig{
+	r := NewRemote(memory.NewStorage(), &config.RemoteConfig{
 		URLs: []string{s.GetLocalRepositoryURL(fixtures.ByTag("tags").One())},
 	})
 
@@ -119,7 +119,7 @@
 }
 
 func (s *RemoteSuite) TestFetchContext(c *C) {
-	r := newRemote(memory.NewStorage(), &config.RemoteConfig{
+	r := NewRemote(memory.NewStorage(), &config.RemoteConfig{
 		URLs: []string{s.GetLocalRepositoryURL(fixtures.ByTag("tags").One())},
 	})
 
@@ -135,7 +135,7 @@
 }
 
 func (s *RemoteSuite) TestFetchWithAllTags(c *C) {
-	r := newRemote(memory.NewStorage(), &config.RemoteConfig{
+	r := NewRemote(memory.NewStorage(), &config.RemoteConfig{
 		URLs: []string{s.GetLocalRepositoryURL(fixtures.ByTag("tags").One())},
 	})
 
@@ -155,7 +155,7 @@
 }
 
 func (s *RemoteSuite) TestFetchWithNoTags(c *C) {
-	r := newRemote(memory.NewStorage(), &config.RemoteConfig{
+	r := NewRemote(memory.NewStorage(), &config.RemoteConfig{
 		URLs: []string{s.GetLocalRepositoryURL(fixtures.ByTag("tags").One())},
 	})
 
@@ -171,7 +171,7 @@
 }
 
 func (s *RemoteSuite) TestFetchWithDepth(c *C) {
-	r := newRemote(memory.NewStorage(), &config.RemoteConfig{
+	r := NewRemote(memory.NewStorage(), &config.RemoteConfig{
 		URLs: []string{s.GetBasicLocalRepositoryURL()},
 	})
 
@@ -212,7 +212,7 @@
 	sto := memory.NewStorage()
 	buf := bytes.NewBuffer(nil)
 
-	r := newRemote(sto, &config.RemoteConfig{Name: "foo", URLs: []string{url}})
+	r := NewRemote(sto, &config.RemoteConfig{Name: "foo", URLs: []string{url}})
 
 	refspec := config.RefSpec("+refs/heads/*:refs/remotes/origin/*")
 	err := r.Fetch(&FetchOptions{
@@ -248,7 +248,7 @@
 	mock := &mockPackfileWriter{Storer: fss}
 
 	url := s.GetBasicLocalRepositoryURL()
-	r := newRemote(mock, &config.RemoteConfig{Name: "foo", URLs: []string{url}})
+	r := NewRemote(mock, &config.RemoteConfig{Name: "foo", URLs: []string{url}})
 
 	refspec := config.RefSpec("+refs/heads/*:refs/remotes/origin/*")
 	err = r.Fetch(&FetchOptions{
@@ -276,7 +276,7 @@
 }
 
 func (s *RemoteSuite) TestFetchNoErrAlreadyUpToDateButStillUpdateLocalRemoteRefs(c *C) {
-	r := newRemote(memory.NewStorage(), &config.RemoteConfig{
+	r := NewRemote(memory.NewStorage(), &config.RemoteConfig{
 		URLs: []string{s.GetBasicLocalRepositoryURL()},
 	})
 
@@ -313,7 +313,7 @@
 }
 
 func (s *RemoteSuite) doTestFetchNoErrAlreadyUpToDate(c *C, url string) {
-	r := newRemote(memory.NewStorage(), &config.RemoteConfig{URLs: []string{url}})
+	r := NewRemote(memory.NewStorage(), &config.RemoteConfig{URLs: []string{url}})
 
 	o := &FetchOptions{
 		RefSpecs: []config.RefSpec{
@@ -328,7 +328,7 @@
 }
 
 func (s *RemoteSuite) testFetchFastForward(c *C, sto storage.Storer) {
-	r := newRemote(sto, &config.RemoteConfig{
+	r := NewRemote(sto, &config.RemoteConfig{
 		URLs: []string{s.GetBasicLocalRepositoryURL()},
 	})
 
@@ -386,7 +386,7 @@
 }
 
 func (s *RemoteSuite) TestString(c *C) {
-	r := newRemote(nil, &config.RemoteConfig{
+	r := NewRemote(nil, &config.RemoteConfig{
 		Name: "foo",
 		URLs: []string{"https://github.com/git-fixtures/basic.git"},
 	})
@@ -405,7 +405,7 @@
 	srcFs := fixtures.Basic().One().DotGit()
 	sto := filesystem.NewStorage(srcFs, cache.NewObjectLRUDefault())
 
-	r := newRemote(sto, &config.RemoteConfig{
+	r := NewRemote(sto, &config.RemoteConfig{
 		Name: DefaultRemoteName,
 		URLs: []string{url},
 	})
@@ -442,7 +442,7 @@
 	fs := fixtures.ByURL("https://github.com/git-fixtures/tags.git").One().DotGit()
 	sto := filesystem.NewStorage(fs, cache.NewObjectLRUDefault())
 
-	r := newRemote(sto, &config.RemoteConfig{
+	r := NewRemote(sto, &config.RemoteConfig{
 		Name: DefaultRemoteName,
 		URLs: []string{url},
 	})
@@ -471,7 +471,7 @@
 	fs := fixtures.ByURL("https://github.com/git-fixtures/tags.git").One().DotGit()
 	sto := filesystem.NewStorage(fs, cache.NewObjectLRUDefault())
 
-	r := newRemote(sto, &config.RemoteConfig{
+	r := NewRemote(sto, &config.RemoteConfig{
 		Name: DefaultRemoteName,
 		URLs: []string{url},
 	})
@@ -494,7 +494,7 @@
 	fs := fixtures.Basic().One().DotGit()
 	sto := filesystem.NewStorage(fs, cache.NewObjectLRUDefault())
 
-	r := newRemote(sto, &config.RemoteConfig{
+	r := NewRemote(sto, &config.RemoteConfig{
 		Name: DefaultRemoteName,
 		URLs: []string{fs.Root()},
 	})
@@ -564,7 +564,7 @@
 	dstSto := filesystem.NewStorage(dstFs, cache.NewObjectLRUDefault())
 
 	url := dstFs.Root()
-	r := newRemote(sto, &config.RemoteConfig{
+	r := NewRemote(sto, &config.RemoteConfig{
 		Name: DefaultRemoteName,
 		URLs: []string{url},
 	})
@@ -711,32 +711,32 @@
 }
 
 func (s *RemoteSuite) TestPushInvalidEndpoint(c *C) {
-	r := newRemote(nil, &config.RemoteConfig{Name: "foo", URLs: []string{"http://\\"}})
+	r := NewRemote(nil, &config.RemoteConfig{Name: "foo", URLs: []string{"http://\\"}})
 	err := r.Push(&PushOptions{RemoteName: "foo"})
 	c.Assert(err, ErrorMatches, ".*invalid character.*")
 }
 
 func (s *RemoteSuite) TestPushNonExistentEndpoint(c *C) {
-	r := newRemote(nil, &config.RemoteConfig{Name: "foo", URLs: []string{"ssh://non-existent/foo.git"}})
+	r := NewRemote(nil, &config.RemoteConfig{Name: "foo", URLs: []string{"ssh://non-existent/foo.git"}})
 	err := r.Push(&PushOptions{})
 	c.Assert(err, NotNil)
 }
 
 func (s *RemoteSuite) TestPushInvalidSchemaEndpoint(c *C) {
-	r := newRemote(nil, &config.RemoteConfig{Name: "origin", URLs: []string{"qux://foo"}})
+	r := NewRemote(nil, &config.RemoteConfig{Name: "origin", URLs: []string{"qux://foo"}})
 	err := r.Push(&PushOptions{})
 	c.Assert(err, ErrorMatches, ".*unsupported scheme.*")
 }
 
 func (s *RemoteSuite) TestPushInvalidFetchOptions(c *C) {
-	r := newRemote(nil, &config.RemoteConfig{Name: "foo", URLs: []string{"qux://foo"}})
+	r := NewRemote(nil, &config.RemoteConfig{Name: "foo", URLs: []string{"qux://foo"}})
 	invalid := config.RefSpec("^*$ñ")
 	err := r.Push(&PushOptions{RefSpecs: []config.RefSpec{invalid}})
 	c.Assert(err, Equals, config.ErrRefSpecMalformedSeparator)
 }
 
 func (s *RemoteSuite) TestPushInvalidRefSpec(c *C) {
-	r := newRemote(nil, &config.RemoteConfig{
+	r := NewRemote(nil, &config.RemoteConfig{
 		Name: DefaultRemoteName,
 		URLs: []string{"some-url"},
 	})
@@ -749,7 +749,7 @@
 }
 
 func (s *RemoteSuite) TestPushWrongRemoteName(c *C) {
-	r := newRemote(nil, &config.RemoteConfig{
+	r := NewRemote(nil, &config.RemoteConfig{
 		Name: DefaultRemoteName,
 		URLs: []string{"some-url"},
 	})
@@ -786,7 +786,7 @@
 
 func (s *RemoteSuite) TestList(c *C) {
 	repo := fixtures.Basic().One()
-	remote := newRemote(memory.NewStorage(), &config.RemoteConfig{
+	remote := NewRemote(memory.NewStorage(), &config.RemoteConfig{
 		Name: DefaultRemoteName,
 		URLs: []string{repo.URL},
 	})
@@ -841,7 +841,7 @@
 		{nil, hashes[0:6]},
 	}
 
-	remote := newRemote(memory.NewStorage(), &config.RemoteConfig{
+	remote := NewRemote(memory.NewStorage(), &config.RemoteConfig{
 		Name: DefaultRemoteName,
 	})
 
@@ -874,7 +874,7 @@
 	fs := fixtures.ByURL("https://github.com/git-fixtures/tags.git").One().DotGit()
 	sto := filesystem.NewStorage(fs, cache.NewObjectLRUDefault())
 
-	r := newRemote(sto, &config.RemoteConfig{
+	r := NewRemote(sto, &config.RemoteConfig{
 		Name: DefaultRemoteName,
 		URLs: []string{url},
 	})
diff --git a/repository.go b/repository.go
index a94dc2f..2251d6c 100644
--- a/repository.go
+++ b/repository.go
@@ -451,7 +451,7 @@
 		return nil, ErrRemoteNotFound
 	}
 
-	return newRemote(r.Storer, c), nil
+	return NewRemote(r.Storer, c), nil
 }
 
 // Remotes returns a list with all the remotes
@@ -465,7 +465,7 @@
 
 	var i int
 	for _, c := range cfg.Remotes {
-		remotes[i] = newRemote(r.Storer, c)
+		remotes[i] = NewRemote(r.Storer, c)
 		i++
 	}
 
@@ -478,7 +478,7 @@
 		return nil, err
 	}
 
-	remote := newRemote(r.Storer, c)
+	remote := NewRemote(r.Storer, c)
 
 	cfg, err := r.Storer.Config()
 	if err != nil {
@@ -504,7 +504,7 @@
 		return nil, ErrAnonymousRemoteName
 	}
 
-	remote := newRemote(r.Storer, c)
+	remote := NewRemote(r.Storer, c)
 
 	return remote, nil
 }
diff --git a/repository_test.go b/repository_test.go
index 0148c78..32fa4fa 100644
--- a/repository_test.go
+++ b/repository_test.go
@@ -16,6 +16,7 @@
 	"golang.org/x/crypto/openpgp"
 	"golang.org/x/crypto/openpgp/armor"
 	openpgperr "golang.org/x/crypto/openpgp/errors"
+
 	"gopkg.in/src-d/go-git.v4/config"
 	"gopkg.in/src-d/go-git.v4/plumbing"
 	"gopkg.in/src-d/go-git.v4/plumbing/cache"
@@ -2671,3 +2672,22 @@
 		})
 	}
 }
+
+func BenchmarkPlainClone(b *testing.B) {
+	for i := 0; i < b.N; i++ {
+		t, err := ioutil.TempDir("", "")
+		if err != nil {
+			b.Fatal(err)
+		}
+		_, err = PlainClone(t, false, &CloneOptions{
+			URL:   "https://github.com/knqyf263/vuln-list",
+			Depth: 1,
+		})
+		if err != nil {
+			b.Error(err)
+		}
+		b.StopTimer()
+		os.RemoveAll(t)
+		b.StartTimer()
+	}
+}
diff --git a/storage/filesystem/index.go b/storage/filesystem/index.go
index d04195c..be800ef 100644
--- a/storage/filesystem/index.go
+++ b/storage/filesystem/index.go
@@ -20,8 +20,14 @@
 	}
 
 	defer ioutil.CheckClose(f, &err)
+	bw := bufio.NewWriter(f)
+	defer func() {
+		if e := bw.Flush(); err == nil && e != nil {
+			err = e
+		}
+	}()
 
-	e := index.NewEncoder(f)
+	e := index.NewEncoder(bw)
 	err = e.Encode(idx)
 	return err
 }
diff --git a/worktree.go b/worktree.go
index 1b10449..4a609e9 100644
--- a/worktree.go
+++ b/worktree.go
@@ -9,6 +9,7 @@
 	"os"
 	"path/filepath"
 	"strings"
+	"sync"
 
 	"gopkg.in/src-d/go-git.v4/config"
 	"gopkg.in/src-d/go-git.v4/plumbing"
@@ -304,6 +305,7 @@
 	if err != nil {
 		return err
 	}
+	b := newIndexBuilder(idx)
 
 	changes, err := w.diffTreeWithStaging(t, true)
 	if err != nil {
@@ -330,12 +332,12 @@
 			name = ch.From.String()
 		}
 
-		_, _ = idx.Remove(name)
+		b.Remove(name)
 		if e == nil {
 			continue
 		}
 
-		idx.Entries = append(idx.Entries, &index.Entry{
+		b.Add(&index.Entry{
 			Name: name,
 			Hash: e.Hash,
 			Mode: e.Mode,
@@ -343,6 +345,7 @@
 
 	}
 
+	b.Write(idx)
 	return w.r.Storer.SetIndex(idx)
 }
 
@@ -356,17 +359,19 @@
 	if err != nil {
 		return err
 	}
+	b := newIndexBuilder(idx)
 
 	for _, ch := range changes {
-		if err := w.checkoutChange(ch, t, idx); err != nil {
+		if err := w.checkoutChange(ch, t, b); err != nil {
 			return err
 		}
 	}
 
+	b.Write(idx)
 	return w.r.Storer.SetIndex(idx)
 }
 
-func (w *Worktree) checkoutChange(ch merkletrie.Change, t *object.Tree, idx *index.Index) error {
+func (w *Worktree) checkoutChange(ch merkletrie.Change, t *object.Tree, idx *indexBuilder) error {
 	a, err := ch.Action()
 	if err != nil {
 		return err
@@ -445,7 +450,7 @@
 func (w *Worktree) checkoutChangeSubmodule(name string,
 	a merkletrie.Action,
 	e *object.TreeEntry,
-	idx *index.Index,
+	idx *indexBuilder,
 ) error {
 	switch a {
 	case merkletrie.Modify:
@@ -479,11 +484,11 @@
 	a merkletrie.Action,
 	t *object.Tree,
 	e *object.TreeEntry,
-	idx *index.Index,
+	idx *indexBuilder,
 ) error {
 	switch a {
 	case merkletrie.Modify:
-		_, _ = idx.Remove(name)
+		idx.Remove(name)
 
 		// to apply perm changes the file is deleted, billy doesn't implement
 		// chmod
@@ -508,6 +513,12 @@
 	return nil
 }
 
+var copyBufferPool = sync.Pool{
+	New: func() interface{} {
+		return make([]byte, 32*1024)
+	},
+}
+
 func (w *Worktree) checkoutFile(f *object.File) (err error) {
 	mode, err := f.Mode.ToOSFileMode()
 	if err != nil {
@@ -531,8 +542,9 @@
 	}
 
 	defer ioutil.CheckClose(to, &err)
-
-	_, err = io.Copy(to, from)
+	buf := copyBufferPool.Get().([]byte)
+	_, err = io.CopyBuffer(to, from, buf)
+	copyBufferPool.Put(buf)
 	return
 }
 
@@ -569,19 +581,18 @@
 	return
 }
 
-func (w *Worktree) addIndexFromTreeEntry(name string, f *object.TreeEntry, idx *index.Index) error {
-	_, _ = idx.Remove(name)
-	idx.Entries = append(idx.Entries, &index.Entry{
+func (w *Worktree) addIndexFromTreeEntry(name string, f *object.TreeEntry, idx *indexBuilder) error {
+	idx.Remove(name)
+	idx.Add(&index.Entry{
 		Hash: f.Hash,
 		Name: name,
 		Mode: filemode.Submodule,
 	})
-
 	return nil
 }
 
-func (w *Worktree) addIndexFromFile(name string, h plumbing.Hash, idx *index.Index) error {
-	_, _ = idx.Remove(name)
+func (w *Worktree) addIndexFromFile(name string, h plumbing.Hash, idx *indexBuilder) error {
+	idx.Remove(name)
 	fi, err := w.Filesystem.Lstat(name)
 	if err != nil {
 		return err
@@ -605,8 +616,7 @@
 	if fillSystemInfo != nil {
 		fillSystemInfo(e, fi.Sys())
 	}
-
-	idx.Entries = append(idx.Entries, e)
+	idx.Add(e)
 	return nil
 }
 
@@ -722,7 +732,7 @@
 
 func (w *Worktree) doClean(status Status, opts *CleanOptions, dir string, files []os.FileInfo) error {
 	for _, fi := range files {
-		if fi.Name() == ".git" {
+		if fi.Name() == GitDirName {
 			continue
 		}
 
@@ -913,3 +923,32 @@
 	}
 	return nil
 }
+
+type indexBuilder struct {
+	entries map[string]*index.Entry
+}
+
+func newIndexBuilder(idx *index.Index) *indexBuilder {
+	entries := make(map[string]*index.Entry, len(idx.Entries))
+	for _, e := range idx.Entries {
+		entries[e.Name] = e
+	}
+	return &indexBuilder{
+		entries: entries,
+	}
+}
+
+func (b *indexBuilder) Write(idx *index.Index) {
+	idx.Entries = idx.Entries[:0]
+	for _, e := range b.entries {
+		idx.Entries = append(idx.Entries, e)
+	}
+}
+
+func (b *indexBuilder) Add(e *index.Entry) {
+	b.entries[e.Name] = e
+}
+
+func (b *indexBuilder) Remove(name string) {
+	delete(b.entries, filepath.ToSlash(name))
+}