Skip to content

Commit

Permalink
config: support insteadOf for remotes' URLs (#79)
Browse files Browse the repository at this point in the history
  • Loading branch information
kostyay committed Dec 1, 2020
1 parent d525a51 commit 51cbc24
Show file tree
Hide file tree
Showing 4 changed files with 239 additions and 2 deletions.
73 changes: 72 additions & 1 deletion config/config.go
Expand Up @@ -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.
Expand All @@ -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(),
}

Expand Down Expand Up @@ -231,6 +235,7 @@ const (
authorSection = "author"
committerSection = "committer"
initSection = "init"
urlSection = "url"
fetchKey = "fetch"
urlKey = "url"
bareKey = "bare"
Expand Down Expand Up @@ -270,6 +275,10 @@ func (c *Config) Unmarshal(b []byte) error {
return err
}

if err := c.unmarshalURLs(); err != nil {
return err
}

return c.unmarshalRemotes()
}

Expand Down Expand Up @@ -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
}

Expand Down Expand Up @@ -367,6 +395,7 @@ func (c *Config) Marshal() ([]byte, error) {
c.marshalRemotes()
c.marshalSubmodules()
c.marshalBranches()
c.marshalURLs()
c.marshalInit()

buf := bytes.NewBuffer(nil)
Expand Down Expand Up @@ -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 != "" {
Expand All @@ -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

Expand Down Expand Up @@ -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 {
Expand All @@ -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
}
}
25 changes: 24 additions & 1 deletion config/config_test.go
Expand Up @@ -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"]
Expand All @@ -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()
Expand All @@ -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/*"})
Expand All @@ -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")
Expand All @@ -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"]
Expand All @@ -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
`)
Expand All @@ -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",
Expand All @@ -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)

Expand All @@ -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/*
Expand All @@ -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()
Expand Down
81 changes: 81 additions & 0 deletions 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):]
}
62 changes: 62 additions & 0 deletions 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")
}

0 comments on commit 51cbc24

Please sign in to comment.