Skip to content

Commit 9e83c31

Browse files
committed
Add GitConfig to read boolean, int, multi-values
Some of git configs have multiple values, such as: `push.pushOption`, `include.path`, and `remote.<name>.fetch`. Implement new struct `GitConfig` in `gitconfig.go` to store value(s) in array to support multiple values. Signed-off-by: Jiang Xin <zhiyou.jx@alibaba-inc.com>
1 parent 13943cc commit 9e83c31

5 files changed

Lines changed: 325 additions & 16 deletions

File tree

errors.go

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,3 +32,6 @@ var ErrMissingStartQuote = errors.New("missing start quote")
3232

3333
// ErrMissingClosingBracket indicates that there was a missing closing bracket in section
3434
var ErrMissingClosingBracket = errors.New("missing closing section bracket")
35+
36+
// ErrNotBoolValue indicates fail to convert config variable to bool
37+
var ErrNotBoolValue = errors.New("not a bool value")

git-config.go

Lines changed: 140 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,140 @@
1+
package goconfig
2+
3+
import (
4+
"strconv"
5+
"strings"
6+
)
7+
8+
// GitConfig maps section to key-value pairs
9+
type GitConfig map[string]GitConfigKeys
10+
11+
// GitConfigKeys maps key to values
12+
type GitConfigKeys map[string][]string
13+
14+
// NewGitConfig returns GitConfig with initialized maps
15+
func NewGitConfig() GitConfig {
16+
c := make(GitConfig)
17+
return c
18+
}
19+
20+
// Keys returns all config variable keys (in lower case)
21+
func (v GitConfig) Keys() []string {
22+
allKeys := []string{}
23+
for s, keys := range v {
24+
for key := range keys {
25+
allKeys = append(allKeys, s+"."+key)
26+
}
27+
}
28+
return allKeys
29+
}
30+
31+
// Add will add user input key-value pair
32+
func (v GitConfig) Add(key, value string) {
33+
s, k := toSectionKey(key)
34+
v._add(s, k, value)
35+
}
36+
37+
// _add key/value to config variables
38+
func (v GitConfig) _add(section, key, value string) {
39+
// section, and key are always in lower case
40+
if _, ok := v[section]; !ok {
41+
v[section] = make(GitConfigKeys)
42+
}
43+
44+
if _, ok := v[section][key]; !ok {
45+
v[section][key] = []string{}
46+
}
47+
v[section][key] = append(v[section][key], value)
48+
}
49+
50+
// Get value from key
51+
func (v GitConfig) Get(key string) string {
52+
values := v.GetAll(key)
53+
if values == nil || len(values) == 0 {
54+
return ""
55+
}
56+
return values[len(values)-1]
57+
}
58+
59+
// GetBool gets boolean from key with default value
60+
func (v GitConfig) GetBool(key string, defaultValue bool) (bool, error) {
61+
value := v.Get(key)
62+
if value == "" {
63+
return defaultValue, nil
64+
}
65+
66+
switch strings.ToLower(value) {
67+
case "yes", "true", "on":
68+
return true, nil
69+
case "no", "false", "off":
70+
return false, nil
71+
}
72+
return false, ErrNotBoolValue
73+
}
74+
75+
// GetInt return integer value of key with default
76+
func (v GitConfig) GetInt(key string, defaultValue int) (int, error) {
77+
value := v.Get(key)
78+
if value == "" {
79+
return defaultValue, nil
80+
}
81+
82+
return strconv.Atoi(value)
83+
}
84+
85+
// GetInt64 return int64 value of key with default
86+
func (v GitConfig) GetInt64(key string, defaultValue int64) (int64, error) {
87+
value := v.Get(key)
88+
if value == "" {
89+
return defaultValue, nil
90+
}
91+
92+
return strconv.ParseInt(value, 10, 64)
93+
}
94+
95+
// GetUint64 return uint64 value of key with default
96+
func (v GitConfig) GetUint64(key string, defaultValue uint64) (uint64, error) {
97+
value := v.Get(key)
98+
if value == "" {
99+
return defaultValue, nil
100+
}
101+
102+
return strconv.ParseUint(value, 10, 64)
103+
}
104+
105+
// GetAll gets all values of a key
106+
func (v GitConfig) GetAll(key string) []string {
107+
section, key := toSectionKey(key)
108+
109+
keys := v[section]
110+
if keys != nil {
111+
return keys[key]
112+
}
113+
return nil
114+
}
115+
116+
func dequoteKey(key string) string {
117+
if !strings.ContainsAny(key, "\"'") {
118+
return key
119+
}
120+
121+
keys := []string{}
122+
for _, k := range strings.Split(key, ".") {
123+
keys = append(keys, strings.Trim(k, "\"'"))
124+
125+
}
126+
return strings.Join(keys, ".")
127+
}
128+
129+
// splitKey will split git config variable to section name and key
130+
func toSectionKey(name string) (string, string) {
131+
name = strings.ToLower(dequoteKey(name))
132+
items := strings.Split(name, ".")
133+
134+
if len(items) < 2 {
135+
return "", ""
136+
}
137+
key := items[len(items)-1]
138+
section := strings.Join(items[0:len(items)-1], ".")
139+
return section, key
140+
}

git-config_test.go

Lines changed: 161 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,161 @@
1+
package goconfig
2+
3+
import (
4+
"testing"
5+
6+
"github.com/stretchr/testify/assert"
7+
)
8+
9+
func TestInvalidSectionName(t *testing.T) {
10+
assert := assert.New(t)
11+
12+
data := `# The following section name should have quote, like: [a "b"]
13+
[a b]
14+
c = d`
15+
_, lineno, err := Parse([]byte(data))
16+
assert.Equal(ErrMissingStartQuote, err)
17+
assert.Equal(uint(2), lineno)
18+
}
19+
20+
func TestInvalidKeyWithSpace(t *testing.T) {
21+
assert := assert.New(t)
22+
23+
data := `# keys should not have spaces
24+
[a]
25+
b c = d`
26+
_, lineno, err := Parse([]byte(data))
27+
assert.Equal(ErrInvalidKeyChar, err)
28+
assert.Equal(uint(3), lineno)
29+
}
30+
31+
func TestParseSectionWithSpaces1(t *testing.T) {
32+
assert := assert.New(t)
33+
34+
data := `[ab "cd"]
35+
value1 = x
36+
value2 = x y
37+
value3 = a \"quote
38+
[remote "hello world"]
39+
url = test`
40+
cfg, _, err := Parse([]byte(data))
41+
assert.Nil(err)
42+
assert.Equal("x", cfg.Get("ab.cd.value1"))
43+
assert.Equal("x y", cfg.Get("ab.cd.value2"))
44+
assert.Equal("a \"quote", cfg.Get("ab.cd.value3"))
45+
}
46+
47+
func TestParseSectionWithSpaces2(t *testing.T) {
48+
assert := assert.New(t)
49+
50+
data := `[remote "hello world"]
51+
url = test`
52+
cfg, _, err := Parse([]byte(data))
53+
assert.Nil(err)
54+
assert.Equal("test", cfg.Get("remote.hello world.url"))
55+
assert.Equal("test", cfg.Get(`remote."hello world".url`))
56+
assert.Equal("test", cfg.Get(`"remote.hello world".url`))
57+
assert.Equal("test", cfg.Get(`"remote.hello world.url"`))
58+
}
59+
60+
func TestGetAll(t *testing.T) {
61+
assert := assert.New(t)
62+
63+
data := `[remote "origin"]
64+
url = https://example.com/my/repo.git
65+
fetch = +refs/heads/*:refs/remotes/origin/*
66+
fetch = +refs/tags/*:refs/tags/*`
67+
cfg, _, err := Parse([]byte(data))
68+
assert.Nil(err)
69+
assert.Equal("+refs/tags/*:refs/tags/*", cfg.Get("remote.origin.fetch"))
70+
assert.Equal([]string{
71+
"+refs/heads/*:refs/remotes/origin/*",
72+
"+refs/tags/*:refs/tags/*",
73+
}, cfg.GetAll("remote.origin.fetch"))
74+
75+
}
76+
77+
func TestGetBool(t *testing.T) {
78+
assert := assert.New(t)
79+
80+
data := `[a]
81+
t1 = true
82+
t2 = yes
83+
t3 = on
84+
f1 = false
85+
f2 = no
86+
f3 = off
87+
x1 = 1
88+
x2 = nothing`
89+
90+
cfg, _, err := Parse([]byte(data))
91+
assert.Nil(err)
92+
93+
v, err := cfg.GetBool("a.t1", false)
94+
assert.Nil(err)
95+
assert.True(v)
96+
97+
v, err = cfg.GetBool("a.t2", false)
98+
assert.Nil(err)
99+
assert.True(v)
100+
101+
v, err = cfg.GetBool("a.t3", false)
102+
assert.Nil(err)
103+
assert.True(v)
104+
105+
v, err = cfg.GetBool("a.t4", false)
106+
assert.Nil(err)
107+
assert.False(v)
108+
109+
v, err = cfg.GetBool("a.f1", true)
110+
assert.Nil(err)
111+
assert.False(v)
112+
113+
v, err = cfg.GetBool("a.f2", true)
114+
assert.Nil(err)
115+
assert.False(v)
116+
117+
v, err = cfg.GetBool("a.f3", true)
118+
assert.Nil(err)
119+
assert.False(v)
120+
121+
v, err = cfg.GetBool("a.f4", true)
122+
assert.Nil(err)
123+
assert.True(v)
124+
125+
v, err = cfg.GetBool("a.x1", true)
126+
assert.Equal(ErrNotBoolValue, err)
127+
128+
v, err = cfg.GetBool("a.x2", true)
129+
assert.Equal(ErrNotBoolValue, err)
130+
}
131+
132+
func TestGetInt(t *testing.T) {
133+
assert := assert.New(t)
134+
135+
data := `[a]
136+
i1 = 1
137+
i2 = 100
138+
i3 = abc`
139+
140+
cfg, _, err := Parse([]byte(data))
141+
assert.Nil(err)
142+
143+
v1, err := cfg.GetInt("a.i1", 0)
144+
assert.Nil(err)
145+
assert.Equal(1, v1)
146+
147+
v2, err := cfg.GetInt64("a.i2", 0)
148+
assert.Nil(err)
149+
assert.Equal(int64(100), v2)
150+
151+
v3, err := cfg.GetUint64("a.i2", 0)
152+
assert.Nil(err)
153+
assert.Equal(uint64(100), v3)
154+
155+
_, err = cfg.GetInt("a.i3", 0)
156+
assert.NotNil(err)
157+
158+
v4, err := cfg.GetInt("a.i4", 6700)
159+
assert.Nil(err)
160+
assert.Equal(6700, v4)
161+
}

goconfig.go

Lines changed: 5 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -9,16 +9,16 @@ type parser struct {
99
}
1010

1111
// Parse takes given bytes as configuration file (according to gitconfig syntax)
12-
func Parse(bytes []byte) (map[string]string, uint, error) {
12+
func Parse(bytes []byte) (GitConfig, uint, error) {
1313
parser := &parser{bytes, 1, false}
1414
cfg, err := parser.parse()
1515
return cfg, parser.linenr, err
1616
}
1717

18-
func (cf *parser) parse() (map[string]string, error) {
18+
func (cf *parser) parse() (GitConfig, error) {
1919
bomPtr := 0
2020
comment := false
21-
cfg := map[string]string{}
21+
cfg := NewGitConfig()
2222
name := ""
2323
var err error
2424
for {
@@ -54,18 +54,17 @@ func (cf *parser) parse() (map[string]string, error) {
5454
if err != nil {
5555
return cfg, err
5656
}
57-
name += "."
5857
continue
5958
}
6059
if !isalpha(c) {
6160
return cfg, ErrInvalidKeyChar
6261
}
63-
key := name + string(lower(c))
62+
key := string(lower(c))
6463
value, err := cf.getValue(&key)
6564
if err != nil {
6665
return cfg, err
6766
}
68-
cfg[key] = value
67+
cfg._add(name, key, value)
6968
}
7069
}
7170

0 commit comments

Comments
 (0)