Skip to content
Permalink
Browse files

Parse include config files from include.path

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 5, 2019
1 parent 9e83c31 commit 83a00ae5b8090415985162b6e3381de03532a573
Showing with 394 additions and 20 deletions.
  1. +17 −0 git-config.go
  2. +39 −7 git-config_test.go
  3. +47 −6 goconfig.go
  4. +7 −7 goconfig_test.go
  5. +75 −0 path.go
  6. +209 −0 path_test.go
@@ -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
}
@@ -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)
}
@@ -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)
}
@@ -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"))
@@ -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`))
@@ -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{
@@ -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)
@@ -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)
@@ -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"))
}
@@ -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
}
@@ -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)
}
}
}
}

@@ -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
@@ -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()
@@ -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()
@@ -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()
@@ -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)
}
@@ -93,6 +93,6 @@ func BenchmarkParse(b *testing.B) {

b.ResetTimer()
for n := 0; n < b.N; n++ {
Parse(bytes)
Parse(bytes, gitconfig)
}
}
75 path.go
@@ -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))
}
Oops, something went wrong.

0 comments on commit 83a00ae

Please sign in to comment.
You can’t perform that action at this time.