diff options
-rw-r--r-- | config/config.go | 8 | ||||
-rw-r--r-- | internal/url/url.go | 6 | ||||
-rwxr-xr-x | internal/url/url_test.go | 107 | ||||
-rw-r--r-- | options.go | 8 | ||||
-rw-r--r-- | plumbing/transport/common_test.go | 20 | ||||
-rw-r--r-- | repository.go | 14 | ||||
-rw-r--r-- | repository_test.go | 29 |
7 files changed, 153 insertions, 39 deletions
diff --git a/config/config.go b/config/config.go index 83629fc..60dfca4 100644 --- a/config/config.go +++ b/config/config.go @@ -264,6 +264,7 @@ const ( defaultBranchKey = "defaultBranch" repositoryFormatVersionKey = "repositoryformatversion" objectFormat = "objectformat" + mirrorKey = "mirror" // DefaultPackWindow holds the number of previous objects used to // generate deltas. The value 10 is the same used by git command. @@ -578,6 +579,8 @@ type RemoteConfig struct { // URLs the URLs of a remote repository. It must be non-empty. Fetch will // always use the first URL, while push will use all of them. URLs []string + // Mirror indicates that the repository is a mirror of remote. + Mirror bool // insteadOfRulesApplied have urls been modified insteadOfRulesApplied bool @@ -631,6 +634,7 @@ func (c *RemoteConfig) unmarshal(s *format.Subsection) error { c.Name = c.raw.Name c.URLs = append([]string(nil), c.raw.Options.GetAll(urlKey)...) c.Fetch = fetch + c.Mirror = c.raw.Options.Get(mirrorKey) == "true" return nil } @@ -663,6 +667,10 @@ func (c *RemoteConfig) marshal() *format.Subsection { c.raw.SetOption(fetchKey, values...) } + if c.Mirror { + c.raw.SetOption(mirrorKey, strconv.FormatBool(c.Mirror)) + } + return c.raw } diff --git a/internal/url/url.go b/internal/url/url.go index 14cf133..2662448 100644 --- a/internal/url/url.go +++ b/internal/url/url.go @@ -5,8 +5,10 @@ import ( ) var ( - isSchemeRegExp = regexp.MustCompile(`^[^:]+://`) - scpLikeUrlRegExp = regexp.MustCompile(`^(?:(?P<user>[^@]+)@)?(?P<host>[^:\s]+):(?:(?P<port>[0-9]{1,5})(?:\/|:))?(?P<path>[^\\].*\/[^\\].*)$`) + isSchemeRegExp = regexp.MustCompile(`^[^:]+://`) + + // Ref: https://github.com/git/git/blob/master/Documentation/urls.txt#L37 + scpLikeUrlRegExp = regexp.MustCompile(`^(?:(?P<user>[^@]+)@)?(?P<host>[^:\s]+):(?:(?P<port>[0-9]{1,5}):)?(?P<path>[^\\].*)$`) ) // MatchesScheme returns true if the given string matches a URL-like diff --git a/internal/url/url_test.go b/internal/url/url_test.go index d168db6..29c3f3e 100755 --- a/internal/url/url_test.go +++ b/internal/url/url_test.go @@ -13,11 +13,27 @@ type URLSuite struct{} var _ = Suite(&URLSuite{}) func (s *URLSuite) TestMatchesScpLike(c *C) { + // See https://github.com/git/git/blob/master/Documentation/urls.txt#L37 examples := []string{ + // Most-extended case "git@github.com:james/bond", - "git@github.com:007/bond", + // Most-extended case with port "git@github.com:22:james/bond", + // Most-extended case with numeric path + "git@github.com:007/bond", + // Most-extended case with port and numeric "username" "git@github.com:22:007/bond", + // Single repo path + "git@github.com:bond", + // Single repo path with port + "git@github.com:22:bond", + // Single repo path with port and numeric repo + "git@github.com:22:007", + // Repo path ending with .git and starting with _ + "git@github.com:22:_007.git", + "git@github.com:_007.git", + "git@github.com:_james.git", + "git@github.com:_james/bond.git", } for _, url := range examples { @@ -26,35 +42,68 @@ func (s *URLSuite) TestMatchesScpLike(c *C) { } func (s *URLSuite) TestFindScpLikeComponents(c *C) { - url := "git@github.com:james/bond" - user, host, port, path := FindScpLikeComponents(url) - - c.Check(user, Equals, "git") - c.Check(host, Equals, "github.com") - c.Check(port, Equals, "") - c.Check(path, Equals, "james/bond") - - url = "git@github.com:007/bond" - user, host, port, path = FindScpLikeComponents(url) - - c.Check(user, Equals, "git") - c.Check(host, Equals, "github.com") - c.Check(port, Equals, "") - c.Check(path, Equals, "007/bond") - - url = "git@github.com:22:james/bond" - user, host, port, path = FindScpLikeComponents(url) + testCases := []struct { + url, user, host, port, path string + }{ + { + // Most-extended case + url: "git@github.com:james/bond", user: "git", host: "github.com", port: "", path: "james/bond", + }, + { + // Most-extended case with port + url: "git@github.com:22:james/bond", user: "git", host: "github.com", port: "22", path: "james/bond", + }, + { + // Most-extended case with numeric path + url: "git@github.com:007/bond", user: "git", host: "github.com", port: "", path: "007/bond", + }, + { + // Most-extended case with port and numeric path + url: "git@github.com:22:007/bond", user: "git", host: "github.com", port: "22", path: "007/bond", + }, + { + // Single repo path + url: "git@github.com:bond", user: "git", host: "github.com", port: "", path: "bond", + }, + { + // Single repo path with port + url: "git@github.com:22:bond", user: "git", host: "github.com", port: "22", path: "bond", + }, + { + // Single repo path with port and numeric path + url: "git@github.com:22:007", user: "git", host: "github.com", port: "22", path: "007", + }, + { + // Repo path ending with .git and starting with _ + url: "git@github.com:22:_007.git", user: "git", host: "github.com", port: "22", path: "_007.git", + }, + { + // Repo path ending with .git and starting with _ + url: "git@github.com:_007.git", user: "git", host: "github.com", port: "", path: "_007.git", + }, + { + // Repo path ending with .git and starting with _ + url: "git@github.com:_james.git", user: "git", host: "github.com", port: "", path: "_james.git", + }, + { + // Repo path ending with .git and starting with _ + url: "git@github.com:_james/bond.git", user: "git", host: "github.com", port: "", path: "_james/bond.git", + }, + } - c.Check(user, Equals, "git") - c.Check(host, Equals, "github.com") - c.Check(port, Equals, "22") - c.Check(path, Equals, "james/bond") + for _, tc := range testCases { + user, host, port, path := FindScpLikeComponents(tc.url) - url = "git@github.com:22:007/bond" - user, host, port, path = FindScpLikeComponents(url) + logf := func(ok bool) { + if ok { + return + } + c.Logf("%q check failed", tc.url) + } - c.Check(user, Equals, "git") - c.Check(host, Equals, "github.com") - c.Check(port, Equals, "22") - c.Check(path, Equals, "007/bond") + logf(c.Check(user, Equals, tc.user)) + logf(c.Check(host, Equals, tc.host)) + logf(c.Check(port, Equals, tc.port)) + logf(c.Check(path, Equals, tc.path)) + } } @@ -46,6 +46,14 @@ type CloneOptions struct { ReferenceName plumbing.ReferenceName // Fetch only ReferenceName if true. SingleBranch bool + // Mirror clones the repository as a mirror. + // + // Compared to a bare clone, mirror not only maps local branches of the + // source to local branches of the target, it maps all refs (including + // remote-tracking branches, notes etc.) and sets up a refspec configuration + // such that all these refs are overwritten by a git remote update in the + // target repository. + Mirror bool // No checkout of HEAD after clone if true. NoCheckout bool // Limit fetching to the specified number of commits. diff --git a/plumbing/transport/common_test.go b/plumbing/transport/common_test.go index 0c5a01a..db11303 100644 --- a/plumbing/transport/common_test.go +++ b/plumbing/transport/common_test.go @@ -95,16 +95,28 @@ func (s *SuiteCommon) TestNewEndpointSCPLike(c *C) { c.Assert(e.String(), Equals, "ssh://git@github.com/user/repository.git") } -func (s *SuiteCommon) TestNewEndpointSCPLikeWithPort(c *C) { +func (s *SuiteCommon) TestNewEndpointSCPLikeWithNumericPath(c *C) { e, err := NewEndpoint("git@github.com:9999/user/repository.git") c.Assert(err, IsNil) c.Assert(e.Protocol, Equals, "ssh") c.Assert(e.User, Equals, "git") c.Assert(e.Password, Equals, "") c.Assert(e.Host, Equals, "github.com") - c.Assert(e.Port, Equals, 9999) - c.Assert(e.Path, Equals, "user/repository.git") - c.Assert(e.String(), Equals, "ssh://git@github.com:9999/user/repository.git") + c.Assert(e.Port, Equals, 22) + c.Assert(e.Path, Equals, "9999/user/repository.git") + c.Assert(e.String(), Equals, "ssh://git@github.com/9999/user/repository.git") +} + +func (s *SuiteCommon) TestNewEndpointSCPLikeWithPort(c *C) { + e, err := NewEndpoint("git@github.com:8080:9999/user/repository.git") + c.Assert(err, IsNil) + c.Assert(e.Protocol, Equals, "ssh") + c.Assert(e.User, Equals, "git") + c.Assert(e.Password, Equals, "") + c.Assert(e.Host, Equals, "github.com") + c.Assert(e.Port, Equals, 8080) + c.Assert(e.Path, Equals, "9999/user/repository.git") + c.Assert(e.String(), Equals, "ssh://git@github.com:8080/9999/user/repository.git") } func (s *SuiteCommon) TestNewEndpointFileAbs(c *C) { diff --git a/repository.go b/repository.go index 56ae976..e009c5d 100644 --- a/repository.go +++ b/repository.go @@ -444,6 +444,9 @@ func PlainCloneContext(ctx context.Context, path string, isBare bool, o *CloneOp return nil, err } + if o.Mirror { + isBare = true + } r, err := PlainInit(path, isBare) if err != nil { return nil, err @@ -851,9 +854,10 @@ func (r *Repository) clone(ctx context.Context, o *CloneOptions) error { } c := &config.RemoteConfig{ - Name: o.RemoteName, - URLs: []string{o.URL}, - Fetch: r.cloneRefSpec(o), + Name: o.RemoteName, + URLs: []string{o.URL}, + Fetch: r.cloneRefSpec(o), + Mirror: o.Mirror, } if _, err := r.CreateRemote(c); err != nil { @@ -906,7 +910,7 @@ func (r *Repository) clone(ctx context.Context, o *CloneOptions) error { return err } - if ref.Name().IsBranch() { + if !o.Mirror && ref.Name().IsBranch() { branchRef := ref.Name() branchName := strings.Split(string(branchRef), "refs/heads/")[1] @@ -937,6 +941,8 @@ const ( func (r *Repository) cloneRefSpec(o *CloneOptions) []config.RefSpec { switch { + case o.Mirror: + return []config.RefSpec{"+refs/*:refs/*"} case o.ReferenceName.IsTag(): return []config.RefSpec{ config.RefSpec(fmt.Sprintf(refspecTag, o.ReferenceName.Short())), diff --git a/repository_test.go b/repository_test.go index 468ce33..ed3e7e6 100644 --- a/repository_test.go +++ b/repository_test.go @@ -189,6 +189,35 @@ func (s *RepositorySuite) TestCloneContext(c *C) { c.Assert(err, Equals, context.Canceled) } +func (s *RepositorySuite) TestCloneMirror(c *C) { + r, err := Clone(memory.NewStorage(), nil, &CloneOptions{ + URL: fixtures.Basic().One().URL, + Mirror: true, + }) + + c.Assert(err, IsNil) + + refs, err := r.References() + var count int + refs.ForEach(func(r *plumbing.Reference) error { c.Log(r); count++; return nil }) + c.Assert(err, IsNil) + // 6 refs total from github.com/git-fixtures/basic.git: + // - HEAD + // - refs/heads/master + // - refs/heads/branch + // - refs/pull/1/head + // - refs/pull/2/head + // - refs/pull/2/merge + c.Assert(count, Equals, 6) + + cfg, err := r.Config() + c.Assert(err, IsNil) + + c.Assert(cfg.Core.IsBare, Equals, true) + c.Assert(cfg.Remotes[DefaultRemoteName].Validate(), IsNil) + c.Assert(cfg.Remotes[DefaultRemoteName].Mirror, Equals, true) +} + func (s *RepositorySuite) TestCloneWithTags(c *C) { url := s.GetLocalRepositoryURL( fixtures.ByURL("https://github.com/git-fixtures/tags.git").One(), |