Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

config: support insteadOf for remotes' URLs #79

Merged
merged 3 commits into from Dec 1, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
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")
}