diff options
author | Kostya Ostrovsky <kostyay@users.noreply.github.com> | 2020-12-01 11:52:53 +0200 |
---|---|---|
committer | GitHub <noreply@github.com> | 2020-12-01 10:52:53 +0100 |
commit | 51cbc24bbecfecbbcea9cd733ad44eaf74b8ae4b (patch) | |
tree | 73a4fdcb346d3233181781e37d1929b457a65216 | |
parent | d525a514057f97bc2b183e2c67f542dd6f0ac0aa (diff) | |
download | go-git-51cbc24bbecfecbbcea9cd733ad44eaf74b8ae4b.tar.gz |
config: support insteadOf for remotes' URLs (#79)
-rw-r--r-- | config/config.go | 73 | ||||
-rw-r--r-- | config/config_test.go | 25 | ||||
-rw-r--r-- | config/url.go | 81 | ||||
-rw-r--r-- | config/url_test.go | 62 |
4 files changed, 239 insertions, 2 deletions
diff --git a/config/config.go b/config/config.go index 8a99e8d..1f737b5 100644 --- a/config/config.go +++ b/config/config.go @@ -105,6 +105,9 @@ type Config struct { // Branches list of branches, the key is the branch name and should // equal Branch.Name Branches map[string]*Branch + // URLs list of url rewrite rules, if repo url starts with URL.InsteadOf value, it will be replaced with the + // key instead. + URLs map[string]*URL // Raw contains the raw information of a config file. The main goal is // preserve the parsed information from the original format, to avoid // dropping unsupported fields. @@ -117,6 +120,7 @@ func NewConfig() *Config { Remotes: make(map[string]*RemoteConfig), Submodules: make(map[string]*Submodule), Branches: make(map[string]*Branch), + URLs: make(map[string]*URL), Raw: format.New(), } @@ -231,6 +235,7 @@ const ( authorSection = "author" committerSection = "committer" initSection = "init" + urlSection = "url" fetchKey = "fetch" urlKey = "url" bareKey = "bare" @@ -270,6 +275,10 @@ func (c *Config) Unmarshal(b []byte) error { return err } + if err := c.unmarshalURLs(); err != nil { + return err + } + return c.unmarshalRemotes() } @@ -323,6 +332,25 @@ func (c *Config) unmarshalRemotes() error { c.Remotes[r.Name] = r } + // Apply insteadOf url rules + for _, r := range c.Remotes { + r.applyURLRules(c.URLs) + } + + return nil +} + +func (c *Config) unmarshalURLs() error { + s := c.Raw.Section(urlSection) + for _, sub := range s.Subsections { + r := &URL{} + if err := r.unmarshal(sub); err != nil { + return err + } + + c.URLs[r.Name] = r + } + return nil } @@ -367,6 +395,7 @@ func (c *Config) Marshal() ([]byte, error) { c.marshalRemotes() c.marshalSubmodules() c.marshalBranches() + c.marshalURLs() c.marshalInit() buf := bytes.NewBuffer(nil) @@ -491,6 +520,20 @@ func (c *Config) marshalBranches() { s.Subsections = newSubsections } +func (c *Config) marshalURLs() { + s := c.Raw.Section(urlSection) + s.Subsections = make(format.Subsections, len(c.URLs)) + + var i int + for _, r := range c.URLs { + section := r.marshal() + // the submodule section at config is a subset of the .gitmodule file + // we should remove the non-valid options for the config file. + s.Subsections[i] = section + i++ + } +} + func (c *Config) marshalInit() { s := c.Raw.Section(initSection) if c.Init.DefaultBranch != "" { @@ -505,6 +548,12 @@ 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 + + // insteadOfRulesApplied have urls been modified + insteadOfRulesApplied bool + // originalURLs are the urls before applying insteadOf rules + originalURLs []string + // Fetch the default set of "refspec" for fetch operation Fetch []RefSpec @@ -565,7 +614,12 @@ func (c *RemoteConfig) marshal() *format.Subsection { if len(c.URLs) == 0 { c.raw.RemoveOption(urlKey) } else { - c.raw.SetOption(urlKey, c.URLs...) + urls := c.URLs + if c.insteadOfRulesApplied { + urls = c.originalURLs + } + + c.raw.SetOption(urlKey, urls...) } if len(c.Fetch) == 0 { @@ -585,3 +639,20 @@ func (c *RemoteConfig) marshal() *format.Subsection { func (c *RemoteConfig) IsFirstURLLocal() bool { return url.IsLocalEndpoint(c.URLs[0]) } + +func (c *RemoteConfig) applyURLRules(urlRules map[string]*URL) { + // save original urls + originalURLs := make([]string, len(c.URLs)) + copy(originalURLs, c.URLs) + + for i, url := range c.URLs { + if matchingURLRule := findLongestInsteadOfMatch(url, urlRules); matchingURLRule != nil { + c.URLs[i] = matchingURLRule.ApplyInsteadOf(c.URLs[i]) + c.insteadOfRulesApplied = true + } + } + + if c.insteadOfRulesApplied { + c.originalURLs = originalURLs + } +} diff --git a/config/config_test.go b/config/config_test.go index 8108a94..b4a9ac3 100644 --- a/config/config_test.go +++ b/config/config_test.go @@ -38,6 +38,8 @@ func (s *ConfigSuite) TestUnmarshal(c *C) { url = git@github.com:src-d/go-git.git fetch = +refs/heads/*:refs/remotes/origin/* fetch = +refs/pull/*:refs/remotes/origin/pull/* +[remote "insteadOf"] + url = https://github.com/kostyay/go-git.git [remote "win-local"] url = X:\\Git\\ [submodule "qux"] @@ -49,6 +51,8 @@ func (s *ConfigSuite) TestUnmarshal(c *C) { merge = refs/heads/master [init] defaultBranch = main +[url "ssh://git@github.com/"] + insteadOf = https://github.com/ `) cfg := NewConfig() @@ -65,7 +69,7 @@ func (s *ConfigSuite) TestUnmarshal(c *C) { c.Assert(cfg.Committer.Name, Equals, "Richard Roe") c.Assert(cfg.Committer.Email, Equals, "richard@example.com") c.Assert(cfg.Pack.Window, Equals, uint(20)) - c.Assert(cfg.Remotes, HasLen, 3) + c.Assert(cfg.Remotes, HasLen, 4) c.Assert(cfg.Remotes["origin"].Name, Equals, "origin") c.Assert(cfg.Remotes["origin"].URLs, DeepEquals, []string{"git@github.com:mcuadros/go-git.git"}) c.Assert(cfg.Remotes["origin"].Fetch, DeepEquals, []RefSpec{"+refs/heads/*:refs/remotes/origin/*"}) @@ -74,6 +78,7 @@ func (s *ConfigSuite) TestUnmarshal(c *C) { c.Assert(cfg.Remotes["alt"].Fetch, DeepEquals, []RefSpec{"+refs/heads/*:refs/remotes/origin/*", "+refs/pull/*:refs/remotes/origin/pull/*"}) c.Assert(cfg.Remotes["win-local"].Name, Equals, "win-local") c.Assert(cfg.Remotes["win-local"].URLs, DeepEquals, []string{"X:\\Git\\"}) + c.Assert(cfg.Remotes["insteadOf"].URLs, DeepEquals, []string{"ssh://git@github.com/kostyay/go-git.git"}) c.Assert(cfg.Submodules, HasLen, 1) c.Assert(cfg.Submodules["qux"].Name, Equals, "qux") c.Assert(cfg.Submodules["qux"].URL, Equals, "https://github.com/foo/qux.git") @@ -94,6 +99,8 @@ func (s *ConfigSuite) TestMarshal(c *C) { url = git@github.com:src-d/go-git.git fetch = +refs/heads/*:refs/remotes/origin/* fetch = +refs/pull/*:refs/remotes/origin/pull/* +[remote "insteadOf"] + url = https://github.com/kostyay/go-git.git [remote "origin"] url = git@github.com:mcuadros/go-git.git [remote "win-local"] @@ -103,6 +110,8 @@ func (s *ConfigSuite) TestMarshal(c *C) { [branch "master"] remote = origin merge = refs/heads/master +[url "ssh://git@github.com/"] + insteadOf = https://github.com/ [init] defaultBranch = main `) @@ -128,6 +137,11 @@ func (s *ConfigSuite) TestMarshal(c *C) { URLs: []string{"X:\\Git\\"}, } + cfg.Remotes["insteadOf"] = &RemoteConfig{ + Name: "insteadOf", + URLs: []string{"https://github.com/kostyay/go-git.git"}, + } + cfg.Submodules["qux"] = &Submodule{ Name: "qux", URL: "https://github.com/foo/qux.git", @@ -139,6 +153,11 @@ func (s *ConfigSuite) TestMarshal(c *C) { Merge: "refs/heads/master", } + cfg.URLs["ssh://git@github.com/"] = &URL{ + Name: "ssh://git@github.com/", + InsteadOf: "https://github.com/", + } + b, err := cfg.Marshal() c.Assert(err, IsNil) @@ -161,6 +180,8 @@ func (s *ConfigSuite) TestUnmarshalMarshal(c *C) { email = richard@example.co [pack] window = 20 +[remote "insteadOf"] + url = https://github.com/kostyay/go-git.git [remote "origin"] url = git@github.com:mcuadros/go-git.git fetch = +refs/heads/*:refs/remotes/origin/* @@ -170,6 +191,8 @@ func (s *ConfigSuite) TestUnmarshalMarshal(c *C) { [branch "master"] remote = origin merge = refs/heads/master +[url "ssh://git@github.com/"] + insteadOf = https://github.com/ `) cfg := NewConfig() diff --git a/config/url.go b/config/url.go new file mode 100644 index 0000000..114d6b2 --- /dev/null +++ b/config/url.go @@ -0,0 +1,81 @@ +package config + +import ( + "errors" + "strings" + + format "github.com/go-git/go-git/v5/plumbing/format/config" +) + +var ( + errURLEmptyInsteadOf = errors.New("url config: empty insteadOf") +) + +// Url defines Url rewrite rules +type URL struct { + // Name new base url + Name string + // Any URL that starts with this value will be rewritten to start, instead, with <base>. + // When more than one insteadOf strings match a given URL, the longest match is used. + InsteadOf string + + // raw representation of the subsection, filled by marshal or unmarshal are + // called. + raw *format.Subsection +} + +// Validate validates fields of branch +func (b *URL) Validate() error { + if b.InsteadOf == "" { + return errURLEmptyInsteadOf + } + + return nil +} + +const ( + insteadOfKey = "insteadOf" +) + +func (u *URL) unmarshal(s *format.Subsection) error { + u.raw = s + + u.Name = s.Name + u.InsteadOf = u.raw.Option(insteadOfKey) + return nil +} + +func (u *URL) marshal() *format.Subsection { + if u.raw == nil { + u.raw = &format.Subsection{} + } + + u.raw.Name = u.Name + u.raw.SetOption(insteadOfKey, u.InsteadOf) + + return u.raw +} + +func findLongestInsteadOfMatch(remoteURL string, urls map[string]*URL) *URL { + var longestMatch *URL + for _, u := range urls { + if !strings.HasPrefix(remoteURL, u.InsteadOf) { + continue + } + + // according to spec if there is more than one match, take the logest + if longestMatch == nil || len(longestMatch.InsteadOf) < len(u.InsteadOf) { + longestMatch = u + } + } + + return longestMatch +} + +func (u *URL) ApplyInsteadOf(url string) string { + if !strings.HasPrefix(url, u.InsteadOf) { + return url + } + + return u.Name + url[len(u.InsteadOf):] +} diff --git a/config/url_test.go b/config/url_test.go new file mode 100644 index 0000000..5afc9f3 --- /dev/null +++ b/config/url_test.go @@ -0,0 +1,62 @@ +package config + +import ( + . "gopkg.in/check.v1" +) + +type URLSuite struct{} + +var _ = Suite(&URLSuite{}) + +func (b *URLSuite) TestValidateInsteadOf(c *C) { + goodURL := URL{ + Name: "ssh://github.com", + InsteadOf: "http://github.com", + } + badURL := URL{} + c.Assert(goodURL.Validate(), IsNil) + c.Assert(badURL.Validate(), NotNil) +} + +func (b *URLSuite) TestMarshal(c *C) { + expected := []byte(`[core] + bare = false +[url "ssh://git@github.com/"] + insteadOf = https://github.com/ +`) + + cfg := NewConfig() + cfg.URLs["ssh://git@github.com/"] = &URL{ + Name: "ssh://git@github.com/", + InsteadOf: "https://github.com/", + } + + actual, err := cfg.Marshal() + c.Assert(err, IsNil) + c.Assert(string(actual), Equals, string(expected)) +} + +func (b *URLSuite) TestUnmarshal(c *C) { + input := []byte(`[core] + bare = false +[url "ssh://git@github.com/"] + insteadOf = https://github.com/ +`) + + cfg := NewConfig() + err := cfg.Unmarshal(input) + c.Assert(err, IsNil) + url := cfg.URLs["ssh://git@github.com/"] + c.Assert(url.Name, Equals, "ssh://git@github.com/") + c.Assert(url.InsteadOf, Equals, "https://github.com/") +} + +func (b *URLSuite) TestApplyInsteadOf(c *C) { + urlRule := URL{ + Name: "ssh://github.com", + InsteadOf: "http://github.com", + } + + c.Assert(urlRule.ApplyInsteadOf("http://google.com"), Equals, "http://google.com") + c.Assert(urlRule.ApplyInsteadOf("http://github.com/myrepo"), Equals, "ssh://github.com/myrepo") +} |