aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorKostya Ostrovsky <kostyay@users.noreply.github.com>2020-12-01 11:52:53 +0200
committerGitHub <noreply@github.com>2020-12-01 10:52:53 +0100
commit51cbc24bbecfecbbcea9cd733ad44eaf74b8ae4b (patch)
tree73a4fdcb346d3233181781e37d1929b457a65216
parentd525a514057f97bc2b183e2c67f542dd6f0ac0aa (diff)
downloadgo-git-51cbc24bbecfecbbcea9cd733ad44eaf74b8ae4b.tar.gz
config: support insteadOf for remotes' URLs (#79)
-rw-r--r--config/config.go73
-rw-r--r--config/config_test.go25
-rw-r--r--config/url.go81
-rw-r--r--config/url_test.go62
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")
+}