Skip to content

Commit

Permalink
Parse include config files from include.path
Browse files Browse the repository at this point in the history
One git config file can include other config files by `include.path`
directions. Parse recursive if there is an `include.path` direction.

Note: not support contional includes yet.

Signed-off-by: Jiang Xin <zhiyou.jx@alibaba-inc.com>
  • Loading branch information
jiangxin committed Mar 11, 2019
1 parent 9e83c31 commit 83a00ae
Show file tree
Hide file tree
Showing 6 changed files with 394 additions and 20 deletions.
17 changes: 17 additions & 0 deletions git-config.go
Original file line number Diff line number Diff line change
Expand Up @@ -138,3 +138,20 @@ func toSectionKey(name string) (string, string) {
section := strings.Join(items[0:len(items)-1], ".")
return section, key
}

// Merge will merge another GitConfig, and new value(s) of the same key will
// append to the end of value list, and new value has higher priority.
func (v GitConfig) Merge(c GitConfig) GitConfig {
for sec, keys := range c {
if _, ok := v[sec]; !ok {
v[sec] = make(GitConfigKeys)
}
for key, values := range keys {
if v[sec][key] == nil {
v[sec][key] = []string{}
}
v[sec][key] = append(v[sec][key], values...)
}
}
return v
}
46 changes: 39 additions & 7 deletions git-config_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ func TestInvalidSectionName(t *testing.T) {
data := `# The following section name should have quote, like: [a "b"]
[a b]
c = d`
_, lineno, err := Parse([]byte(data))
_, lineno, err := Parse([]byte(data), "filename")
assert.Equal(ErrMissingStartQuote, err)
assert.Equal(uint(2), lineno)
}
Expand All @@ -23,7 +23,7 @@ func TestInvalidKeyWithSpace(t *testing.T) {
data := `# keys should not have spaces
[a]
b c = d`
_, lineno, err := Parse([]byte(data))
_, lineno, err := Parse([]byte(data), "filename")
assert.Equal(ErrInvalidKeyChar, err)
assert.Equal(uint(3), lineno)
}
Expand All @@ -37,7 +37,7 @@ func TestParseSectionWithSpaces1(t *testing.T) {
value3 = a \"quote
[remote "hello world"]
url = test`
cfg, _, err := Parse([]byte(data))
cfg, _, err := Parse([]byte(data), "filename")
assert.Nil(err)
assert.Equal("x", cfg.Get("ab.cd.value1"))
assert.Equal("x y", cfg.Get("ab.cd.value2"))
Expand All @@ -49,7 +49,7 @@ func TestParseSectionWithSpaces2(t *testing.T) {

data := `[remote "hello world"]
url = test`
cfg, _, err := Parse([]byte(data))
cfg, _, err := Parse([]byte(data), "filename")
assert.Nil(err)
assert.Equal("test", cfg.Get("remote.hello world.url"))
assert.Equal("test", cfg.Get(`remote."hello world".url`))
Expand All @@ -64,7 +64,7 @@ func TestGetAll(t *testing.T) {
url = https://example.com/my/repo.git
fetch = +refs/heads/*:refs/remotes/origin/*
fetch = +refs/tags/*:refs/tags/*`
cfg, _, err := Parse([]byte(data))
cfg, _, err := Parse([]byte(data), "filename")
assert.Nil(err)
assert.Equal("+refs/tags/*:refs/tags/*", cfg.Get("remote.origin.fetch"))
assert.Equal([]string{
Expand All @@ -87,7 +87,7 @@ func TestGetBool(t *testing.T) {
x1 = 1
x2 = nothing`

cfg, _, err := Parse([]byte(data))
cfg, _, err := Parse([]byte(data), "filename")
assert.Nil(err)

v, err := cfg.GetBool("a.t1", false)
Expand Down Expand Up @@ -137,7 +137,7 @@ func TestGetInt(t *testing.T) {
i2 = 100
i3 = abc`

cfg, _, err := Parse([]byte(data))
cfg, _, err := Parse([]byte(data), "filename")
assert.Nil(err)

v1, err := cfg.GetInt("a.i1", 0)
Expand All @@ -159,3 +159,35 @@ func TestGetInt(t *testing.T) {
assert.Nil(err)
assert.Equal(6700, v4)
}

func TestMerge(t *testing.T) {
assert := assert.New(t)

data := `[a]
b = value-b
c = value-c`

cfg, _, err := Parse([]byte(data), "filename")
assert.Nil(err)

assert.Equal("value-b", cfg.Get("a.b"))
assert.Equal("value-c", cfg.Get("a.c"))

data = `[a]
c = other-c
d = other-d`

cfg2, _, err := Parse([]byte(data), "filename")
assert.Nil(err)
assert.Equal("other-c", cfg2.Get("a.c"))
assert.Equal("other-d", cfg2.Get("a.d"))

cfg.Merge(cfg2)
assert.Equal("value-b", cfg.Get("a.b"))
assert.Equal("other-c", cfg.Get("a.c"))
assert.Equal("other-d", cfg.Get("a.d"))
assert.Equal([]string{
"value-c",
"other-c",
}, cfg.GetAll("a.c"))
}
53 changes: 47 additions & 6 deletions goconfig.go
Original file line number Diff line number Diff line change
@@ -1,16 +1,32 @@
package goconfig

const utf8BOM = "\357\273\277"
import (
"fmt"
"io/ioutil"
"path"
)

const (
utf8BOM = "\357\273\277"
maxIncludeDepth = 10
)

type parser struct {
bytes []byte
linenr uint
eof bool
bytes []byte
linenr uint
eof bool
filename string
depth int
}

// Parse takes given bytes as configuration file (according to gitconfig syntax)
func Parse(bytes []byte) (GitConfig, uint, error) {
parser := &parser{bytes, 1, false}
func Parse(bytes []byte, filename string) (GitConfig, uint, error) {
return runParse(bytes, filename, 1)
}

func runParse(bytes []byte, filename string, depth int) (GitConfig, uint, error) {
parser := &parser{bytes, 1, false, filename, depth}

cfg, err := parser.parse()
return cfg, parser.linenr, err
}
Expand Down Expand Up @@ -65,6 +81,31 @@ func (cf *parser) parse() (GitConfig, error) {
return cfg, err
}
cfg._add(name, key, value)
if name == "include" && key == "path" {
file, err := AbsJoin(path.Dir(cf.filename), value)
if err != nil {
return nil, err
}
// Check circular includes
if cf.depth >= maxIncludeDepth {
return nil, fmt.Errorf("exceeded maximum include depth (%d) while including\n"+
"\t%s\n"+
"from"+
"\t%s\n"+
"This might be due to circular includes\n",
maxIncludeDepth,
cf.filename,
file)
}
bytes, err := ioutil.ReadFile(file)
if err == nil {
config, _, err := runParse(bytes, file, cf.depth+1)
if err != nil {
return cfg, err
}
cfg.Merge(config)
}
}
}
}

Expand Down
14 changes: 7 additions & 7 deletions goconfig_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ func TestDanyel(t *testing.T) {
if err != nil {
t.Fatalf("Reading file %v failed", filename)
}
config, lineno, err := Parse(bytes)
config, lineno, err := Parse(bytes, filename)
assert.Equal(t, nil, err)
assert.Equal(t, 10, int(lineno))
_ = config
Expand All @@ -27,15 +27,15 @@ func TestDanyel(t *testing.T) {

func TestInvalidKey(t *testing.T) {
invalidConfig := ".name = Danyel"
config, lineno, err := Parse([]byte(invalidConfig))
config, lineno, err := Parse([]byte(invalidConfig), "")
assert.Equal(t, ErrInvalidKeyChar, err)
assert.Equal(t, 1, int(lineno))
assert.Equal(t, NewGitConfig(), config)
}

func TestNoNewLine(t *testing.T) {
validConfig := "[user] name = Danyel"
config, lineno, err := Parse([]byte(validConfig))
config, lineno, err := Parse([]byte(validConfig), "")
assert.Equal(t, nil, err)
assert.Equal(t, 1, int(lineno))
expect := NewGitConfig()
Expand All @@ -45,7 +45,7 @@ func TestNoNewLine(t *testing.T) {

func TestUpperCaseKey(t *testing.T) {
validConfig := "[core]\nQuotePath = false\n"
config, lineno, err := Parse([]byte(validConfig))
config, lineno, err := Parse([]byte(validConfig), "")
assert.Equal(t, nil, err)
assert.Equal(t, 3, int(lineno))
expect := NewGitConfig()
Expand All @@ -55,7 +55,7 @@ func TestUpperCaseKey(t *testing.T) {

func TestExtended(t *testing.T) {
validConfig := `[http "https://my-website.com"] sslVerify = false`
config, lineno, err := Parse([]byte(validConfig))
config, lineno, err := Parse([]byte(validConfig), "")
assert.Equal(t, nil, err)
assert.Equal(t, 1, int(lineno))
expect := NewGitConfig()
Expand All @@ -70,7 +70,7 @@ func ExampleParse() {
log.Fatalf("Couldn't read file %v\n", gitconfig)
}

config, lineno, err := Parse(bytes)
config, lineno, err := Parse(bytes, gitconfig)
if err != nil {
log.Fatalf("Error on line %d: %v\n", lineno, err)
}
Expand All @@ -93,6 +93,6 @@ func BenchmarkParse(b *testing.B) {

b.ResetTimer()
for n := 0; n < b.N; n++ {
Parse(bytes)
Parse(bytes, gitconfig)
}
}
75 changes: 75 additions & 0 deletions path.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
package goconfig

import (
"fmt"
"os"
"path/filepath"
"runtime"
)

func homeDir() (string, error) {
var (
home string
)

if runtime.GOOS == "windows" {
home = os.Getenv("USERPROFILE")
if home == "" {
home = os.Getenv("HOMEDRIVE") + os.Getenv("HOMEPATH")
}
}
if home == "" {
home = os.Getenv("HOME")
}

if home == "" {
return "", fmt.Errorf("cannot find HOME")
}

return home, nil
}

func expendHome(name string) (string, error) {
if filepath.IsAbs(name) {
return name, nil
}

home, err := homeDir()
if err != nil {
return "", err
}

if len(name) == 0 || name == "~" {
return home, nil
} else if len(name) > 1 && name[0] == '~' && (name[1] == '/' || name[1] == '\\') {
return filepath.Join(home, name[2:]), nil
}

return filepath.Join(home, name), nil
}

// Abs returns absolute path and will expend homedir if path has "~/' prefix
func Abs(name string) (string, error) {
if filepath.IsAbs(name) {
return name, nil
}

if len(name) > 0 && name[0] == '~' && (len(name) == 1 || name[1] == '/' || name[1] == '\\') {
return expendHome(name)
}

return filepath.Abs(name)
}

// AbsJoin returns absolute path, and use <dir> as parent dir for relative path
func AbsJoin(dir, name string) (string, error) {
if filepath.IsAbs(name) {
return name, nil
}

if len(name) > 0 && name[0] == '~' && (len(name) == 1 || name[1] == '/' || name[1] == '\\') {
return expendHome(name)
}

return Abs(filepath.Join(dir, name))
}

0 comments on commit 83a00ae

Please sign in to comment.