aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--COMPATIBILITY.md2
-rw-r--r--_examples/common_test.go13
-rw-r--r--_examples/ls-remote/main.go42
-rw-r--r--_examples/merge_base/help-long.msg.go63
-rw-r--r--_examples/merge_base/helpers.go61
-rw-r--r--_examples/merge_base/main.go124
-rw-r--r--config/refspec.go8
-rw-r--r--config/refspec_test.go67
-rw-r--r--plumbing/format/index/doc.go4
-rw-r--r--plumbing/format/index/index.go2
-rw-r--r--remote.go5
-rw-r--r--remote_test.go64
-rw-r--r--repository.go8
-rw-r--r--repository_test.go20
-rw-r--r--storage/filesystem/index.go8
-rw-r--r--worktree.go75
16 files changed, 488 insertions, 78 deletions
diff --git a/COMPATIBILITY.md b/COMPATIBILITY.md
index e07e799..4a3da62 100644
--- a/COMPATIBILITY.md
+++ b/COMPATIBILITY.md
@@ -86,7 +86,7 @@ is supported by go-git.
| 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 @@ var args = map[string][]string{
"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 @@ func TestExamples(t *testing.T) {
}
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 @@ func addRemote(local, remote string) {
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 @@ var (
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 @@ func (s RefSpec) matchGlob(n plumbing.ReferenceName) bool {
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) TestRefSpecMatch(c *C) {
}
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) TestRefSpecDst(c *C) {
}
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 @@ type ResolveUndoEntry struct {
}
// 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 @@ type Remote struct {
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 @@ type RemoteSuite struct {
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) TestFetchWildcard(c *C) {
}
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) TestFetchWildcardTags(c *C) {
}
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) TestFetch(c *C) {
}
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) TestFetchNonExistantReference(c *C) {
}
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) TestFetchContext(c *C) {
}
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) TestFetchWithAllTags(c *C) {
}
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) TestFetchWithNoTags(c *C) {
}
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 @@ func (s *RemoteSuite) TestFetchWithProgress(c *C) {
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 @@ func (s *RemoteSuite) TestFetchWithPackfileWriter(c *C) {
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) TestFetchNoErrAlreadyUpToDate(c *C) {
}
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) TestFetchNoErrAlreadyUpToDateWithNonCommitObjects(c *C) {
}
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) doTestFetchNoErrAlreadyUpToDate(c *C, url string) {
}
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) TestFetchFastForwardFS(c *C) {
}
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 @@ func (s *RemoteSuite) TestPushToEmptyRepository(c *C) {
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 @@ func (s *RemoteSuite) TestPushContext(c *C) {
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 @@ func (s *RemoteSuite) TestPushTags(c *C) {
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 @@ func (s *RemoteSuite) TestPushNoErrAlreadyUpToDate(c *C) {
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 @@ func (s *RemoteSuite) TestPushForce(c *C) {
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) TestPushNewReferenceAndDeleteInBatch(c *C) {
}
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) TestPushInvalidRefSpec(c *C) {
}
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) TestGetHaves(c *C) {
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 @@ func (s *RemoteSuite) TestUpdateShallows(c *C) {
{nil, hashes[0:6]},
}
- remote := newRemote(memory.NewStorage(), &config.RemoteConfig{
+ remote := NewRemote(memory.NewStorage(), &config.RemoteConfig{
Name: DefaultRemoteName,
})
@@ -874,7 +874,7 @@ func (s *RemoteSuite) TestUseRefDeltas(c *C) {
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 @@ func (r *Repository) Remote(name string) (*Remote, error) {
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 @@ func (r *Repository) Remotes() ([]*Remote, error) {
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 @@ func (r *Repository) CreateRemote(c *config.RemoteConfig) (*Remote, error) {
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 @@ func (r *Repository) CreateRemoteAnonymous(c *config.RemoteConfig) (*Remote, err
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 @@ import (
"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 BenchmarkObjects(b *testing.B) {
})
}
}
+
+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 @@ func (s *IndexStorage) SetIndex(idx *index.Index) (err error) {
}
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 @@ import (
"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 @@ func (w *Worktree) resetIndex(t *object.Tree) error {
if err != nil {
return err
}
+ b := newIndexBuilder(idx)
changes, err := w.diffTreeWithStaging(t, true)
if err != nil {
@@ -330,12 +332,12 @@ func (w *Worktree) resetIndex(t *object.Tree) error {
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 @@ func (w *Worktree) resetIndex(t *object.Tree) error {
}
+ b.Write(idx)
return w.r.Storer.SetIndex(idx)
}
@@ -356,17 +359,19 @@ func (w *Worktree) resetWorktree(t *object.Tree) error {
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) setHEADCommit(commit plumbing.Hash) error {
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 @@ func (w *Worktree) checkoutChangeRegularFile(name string,
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 @@ func (w *Worktree) checkoutChangeRegularFile(name string,
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 @@ func (w *Worktree) checkoutFile(f *object.File) (err error) {
}
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 @@ func (w *Worktree) checkoutFileSymlink(f *object.File) (err error) {
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 @@ func (w *Worktree) addIndexFromFile(name string, h plumbing.Hash, idx *index.Ind
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) Clean(opts *CleanOptions) error {
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 @@ func doCleanDirectories(fs billy.Filesystem, dir string) error {
}
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))
+}