diff options
-rw-r--r-- | COMPATIBILITY.md | 2 | ||||
-rw-r--r-- | _examples/common_test.go | 13 | ||||
-rw-r--r-- | _examples/ls-remote/main.go | 42 | ||||
-rw-r--r-- | _examples/merge_base/help-long.msg.go | 63 | ||||
-rw-r--r-- | _examples/merge_base/helpers.go | 61 | ||||
-rw-r--r-- | _examples/merge_base/main.go | 124 | ||||
-rw-r--r-- | config/refspec.go | 8 | ||||
-rw-r--r-- | config/refspec_test.go | 67 | ||||
-rw-r--r-- | plumbing/format/index/doc.go | 4 | ||||
-rw-r--r-- | plumbing/format/index/index.go | 2 | ||||
-rw-r--r-- | remote.go | 5 | ||||
-rw-r--r-- | remote_test.go | 64 | ||||
-rw-r--r-- | repository.go | 8 | ||||
-rw-r--r-- | repository_test.go | 20 | ||||
-rw-r--r-- | storage/filesystem/index.go | 8 | ||||
-rw-r--r-- | worktree.go | 75 |
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. // @@ -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)) +} |