Skip to content

Commit 83a00ae

Browse files
committed
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>
1 parent 9e83c31 commit 83a00ae

File tree

6 files changed

+394
-20
lines changed

6 files changed

+394
-20
lines changed

git-config.go

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -138,3 +138,20 @@ func toSectionKey(name string) (string, string) {
138138
section := strings.Join(items[0:len(items)-1], ".")
139139
return section, key
140140
}
141+
142+
// Merge will merge another GitConfig, and new value(s) of the same key will
143+
// append to the end of value list, and new value has higher priority.
144+
func (v GitConfig) Merge(c GitConfig) GitConfig {
145+
for sec, keys := range c {
146+
if _, ok := v[sec]; !ok {
147+
v[sec] = make(GitConfigKeys)
148+
}
149+
for key, values := range keys {
150+
if v[sec][key] == nil {
151+
v[sec][key] = []string{}
152+
}
153+
v[sec][key] = append(v[sec][key], values...)
154+
}
155+
}
156+
return v
157+
}

git-config_test.go

Lines changed: 39 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ func TestInvalidSectionName(t *testing.T) {
1212
data := `# The following section name should have quote, like: [a "b"]
1313
[a b]
1414
c = d`
15-
_, lineno, err := Parse([]byte(data))
15+
_, lineno, err := Parse([]byte(data), "filename")
1616
assert.Equal(ErrMissingStartQuote, err)
1717
assert.Equal(uint(2), lineno)
1818
}
@@ -23,7 +23,7 @@ func TestInvalidKeyWithSpace(t *testing.T) {
2323
data := `# keys should not have spaces
2424
[a]
2525
b c = d`
26-
_, lineno, err := Parse([]byte(data))
26+
_, lineno, err := Parse([]byte(data), "filename")
2727
assert.Equal(ErrInvalidKeyChar, err)
2828
assert.Equal(uint(3), lineno)
2929
}
@@ -37,7 +37,7 @@ func TestParseSectionWithSpaces1(t *testing.T) {
3737
value3 = a \"quote
3838
[remote "hello world"]
3939
url = test`
40-
cfg, _, err := Parse([]byte(data))
40+
cfg, _, err := Parse([]byte(data), "filename")
4141
assert.Nil(err)
4242
assert.Equal("x", cfg.Get("ab.cd.value1"))
4343
assert.Equal("x y", cfg.Get("ab.cd.value2"))
@@ -49,7 +49,7 @@ func TestParseSectionWithSpaces2(t *testing.T) {
4949

5050
data := `[remote "hello world"]
5151
url = test`
52-
cfg, _, err := Parse([]byte(data))
52+
cfg, _, err := Parse([]byte(data), "filename")
5353
assert.Nil(err)
5454
assert.Equal("test", cfg.Get("remote.hello world.url"))
5555
assert.Equal("test", cfg.Get(`remote."hello world".url`))
@@ -64,7 +64,7 @@ func TestGetAll(t *testing.T) {
6464
url = https://example.com/my/repo.git
6565
fetch = +refs/heads/*:refs/remotes/origin/*
6666
fetch = +refs/tags/*:refs/tags/*`
67-
cfg, _, err := Parse([]byte(data))
67+
cfg, _, err := Parse([]byte(data), "filename")
6868
assert.Nil(err)
6969
assert.Equal("+refs/tags/*:refs/tags/*", cfg.Get("remote.origin.fetch"))
7070
assert.Equal([]string{
@@ -87,7 +87,7 @@ func TestGetBool(t *testing.T) {
8787
x1 = 1
8888
x2 = nothing`
8989

90-
cfg, _, err := Parse([]byte(data))
90+
cfg, _, err := Parse([]byte(data), "filename")
9191
assert.Nil(err)
9292

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

140-
cfg, _, err := Parse([]byte(data))
140+
cfg, _, err := Parse([]byte(data), "filename")
141141
assert.Nil(err)
142142

143143
v1, err := cfg.GetInt("a.i1", 0)
@@ -159,3 +159,35 @@ func TestGetInt(t *testing.T) {
159159
assert.Nil(err)
160160
assert.Equal(6700, v4)
161161
}
162+
163+
func TestMerge(t *testing.T) {
164+
assert := assert.New(t)
165+
166+
data := `[a]
167+
b = value-b
168+
c = value-c`
169+
170+
cfg, _, err := Parse([]byte(data), "filename")
171+
assert.Nil(err)
172+
173+
assert.Equal("value-b", cfg.Get("a.b"))
174+
assert.Equal("value-c", cfg.Get("a.c"))
175+
176+
data = `[a]
177+
c = other-c
178+
d = other-d`
179+
180+
cfg2, _, err := Parse([]byte(data), "filename")
181+
assert.Nil(err)
182+
assert.Equal("other-c", cfg2.Get("a.c"))
183+
assert.Equal("other-d", cfg2.Get("a.d"))
184+
185+
cfg.Merge(cfg2)
186+
assert.Equal("value-b", cfg.Get("a.b"))
187+
assert.Equal("other-c", cfg.Get("a.c"))
188+
assert.Equal("other-d", cfg.Get("a.d"))
189+
assert.Equal([]string{
190+
"value-c",
191+
"other-c",
192+
}, cfg.GetAll("a.c"))
193+
}

goconfig.go

Lines changed: 47 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,32 @@
11
package goconfig
22

3-
const utf8BOM = "\357\273\277"
3+
import (
4+
"fmt"
5+
"io/ioutil"
6+
"path"
7+
)
8+
9+
const (
10+
utf8BOM = "\357\273\277"
11+
maxIncludeDepth = 10
12+
)
413

514
type parser struct {
6-
bytes []byte
7-
linenr uint
8-
eof bool
15+
bytes []byte
16+
linenr uint
17+
eof bool
18+
filename string
19+
depth int
920
}
1021

1122
// Parse takes given bytes as configuration file (according to gitconfig syntax)
12-
func Parse(bytes []byte) (GitConfig, uint, error) {
13-
parser := &parser{bytes, 1, false}
23+
func Parse(bytes []byte, filename string) (GitConfig, uint, error) {
24+
return runParse(bytes, filename, 1)
25+
}
26+
27+
func runParse(bytes []byte, filename string, depth int) (GitConfig, uint, error) {
28+
parser := &parser{bytes, 1, false, filename, depth}
29+
1430
cfg, err := parser.parse()
1531
return cfg, parser.linenr, err
1632
}
@@ -65,6 +81,31 @@ func (cf *parser) parse() (GitConfig, error) {
6581
return cfg, err
6682
}
6783
cfg._add(name, key, value)
84+
if name == "include" && key == "path" {
85+
file, err := AbsJoin(path.Dir(cf.filename), value)
86+
if err != nil {
87+
return nil, err
88+
}
89+
// Check circular includes
90+
if cf.depth >= maxIncludeDepth {
91+
return nil, fmt.Errorf("exceeded maximum include depth (%d) while including\n"+
92+
"\t%s\n"+
93+
"from"+
94+
"\t%s\n"+
95+
"This might be due to circular includes\n",
96+
maxIncludeDepth,
97+
cf.filename,
98+
file)
99+
}
100+
bytes, err := ioutil.ReadFile(file)
101+
if err == nil {
102+
config, _, err := runParse(bytes, file, cf.depth+1)
103+
if err != nil {
104+
return cfg, err
105+
}
106+
cfg.Merge(config)
107+
}
108+
}
68109
}
69110
}
70111

goconfig_test.go

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ func TestDanyel(t *testing.T) {
1515
if err != nil {
1616
t.Fatalf("Reading file %v failed", filename)
1717
}
18-
config, lineno, err := Parse(bytes)
18+
config, lineno, err := Parse(bytes, filename)
1919
assert.Equal(t, nil, err)
2020
assert.Equal(t, 10, int(lineno))
2121
_ = config
@@ -27,15 +27,15 @@ func TestDanyel(t *testing.T) {
2727

2828
func TestInvalidKey(t *testing.T) {
2929
invalidConfig := ".name = Danyel"
30-
config, lineno, err := Parse([]byte(invalidConfig))
30+
config, lineno, err := Parse([]byte(invalidConfig), "")
3131
assert.Equal(t, ErrInvalidKeyChar, err)
3232
assert.Equal(t, 1, int(lineno))
3333
assert.Equal(t, NewGitConfig(), config)
3434
}
3535

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

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

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

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

9494
b.ResetTimer()
9595
for n := 0; n < b.N; n++ {
96-
Parse(bytes)
96+
Parse(bytes, gitconfig)
9797
}
9898
}

path.go

Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
package goconfig
2+
3+
import (
4+
"fmt"
5+
"os"
6+
"path/filepath"
7+
"runtime"
8+
)
9+
10+
func homeDir() (string, error) {
11+
var (
12+
home string
13+
)
14+
15+
if runtime.GOOS == "windows" {
16+
home = os.Getenv("USERPROFILE")
17+
if home == "" {
18+
home = os.Getenv("HOMEDRIVE") + os.Getenv("HOMEPATH")
19+
}
20+
}
21+
if home == "" {
22+
home = os.Getenv("HOME")
23+
}
24+
25+
if home == "" {
26+
return "", fmt.Errorf("cannot find HOME")
27+
}
28+
29+
return home, nil
30+
}
31+
32+
func expendHome(name string) (string, error) {
33+
if filepath.IsAbs(name) {
34+
return name, nil
35+
}
36+
37+
home, err := homeDir()
38+
if err != nil {
39+
return "", err
40+
}
41+
42+
if len(name) == 0 || name == "~" {
43+
return home, nil
44+
} else if len(name) > 1 && name[0] == '~' && (name[1] == '/' || name[1] == '\\') {
45+
return filepath.Join(home, name[2:]), nil
46+
}
47+
48+
return filepath.Join(home, name), nil
49+
}
50+
51+
// Abs returns absolute path and will expend homedir if path has "~/' prefix
52+
func Abs(name string) (string, error) {
53+
if filepath.IsAbs(name) {
54+
return name, nil
55+
}
56+
57+
if len(name) > 0 && name[0] == '~' && (len(name) == 1 || name[1] == '/' || name[1] == '\\') {
58+
return expendHome(name)
59+
}
60+
61+
return filepath.Abs(name)
62+
}
63+
64+
// AbsJoin returns absolute path, and use <dir> as parent dir for relative path
65+
func AbsJoin(dir, name string) (string, error) {
66+
if filepath.IsAbs(name) {
67+
return name, nil
68+
}
69+
70+
if len(name) > 0 && name[0] == '~' && (len(name) == 1 || name[1] == '/' || name[1] == '\\') {
71+
return expendHome(name)
72+
}
73+
74+
return Abs(filepath.Join(dir, name))
75+
}

0 commit comments

Comments
 (0)