Skip to content
Permalink
Browse files

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>
  • Loading branch information...
jiangxin committed Feb 28, 2019
1 parent 13943cc commit 9e83c3157189a458a63832c151fbd52222ddd56a
Showing with 325 additions and 16 deletions.
  1. +3 −0 errors.go
  2. +140 −0 git-config.go
  3. +161 −0 git-config_test.go
  4. +5 −6 goconfig.go
  5. +16 −10 goconfig_test.go
@@ -32,3 +32,6 @@ var ErrMissingStartQuote = errors.New("missing start quote")

// ErrMissingClosingBracket indicates that there was a missing closing bracket in section
var ErrMissingClosingBracket = errors.New("missing closing section bracket")

// ErrNotBoolValue indicates fail to convert config variable to bool
var ErrNotBoolValue = errors.New("not a bool value")
@@ -0,0 +1,140 @@
package goconfig

import (
"strconv"
"strings"
)

// GitConfig maps section to key-value pairs
type GitConfig map[string]GitConfigKeys

// GitConfigKeys maps key to values
type GitConfigKeys map[string][]string

// NewGitConfig returns GitConfig with initialized maps
func NewGitConfig() GitConfig {
c := make(GitConfig)
return c
}

// Keys returns all config variable keys (in lower case)
func (v GitConfig) Keys() []string {
allKeys := []string{}
for s, keys := range v {
for key := range keys {
allKeys = append(allKeys, s+"."+key)
}
}
return allKeys
}

// Add will add user input key-value pair
func (v GitConfig) Add(key, value string) {
s, k := toSectionKey(key)
v._add(s, k, value)
}

// _add key/value to config variables
func (v GitConfig) _add(section, key, value string) {
// section, and key are always in lower case
if _, ok := v[section]; !ok {
v[section] = make(GitConfigKeys)
}

if _, ok := v[section][key]; !ok {
v[section][key] = []string{}
}
v[section][key] = append(v[section][key], value)
}

// Get value from key
func (v GitConfig) Get(key string) string {
values := v.GetAll(key)
if values == nil || len(values) == 0 {
return ""
}
return values[len(values)-1]
}

// GetBool gets boolean from key with default value
func (v GitConfig) GetBool(key string, defaultValue bool) (bool, error) {
value := v.Get(key)
if value == "" {
return defaultValue, nil
}

switch strings.ToLower(value) {
case "yes", "true", "on":
return true, nil
case "no", "false", "off":
return false, nil
}
return false, ErrNotBoolValue
}

// GetInt return integer value of key with default
func (v GitConfig) GetInt(key string, defaultValue int) (int, error) {
value := v.Get(key)
if value == "" {
return defaultValue, nil
}

return strconv.Atoi(value)
}

// GetInt64 return int64 value of key with default
func (v GitConfig) GetInt64(key string, defaultValue int64) (int64, error) {
value := v.Get(key)
if value == "" {
return defaultValue, nil
}

return strconv.ParseInt(value, 10, 64)
}

// GetUint64 return uint64 value of key with default
func (v GitConfig) GetUint64(key string, defaultValue uint64) (uint64, error) {
value := v.Get(key)
if value == "" {
return defaultValue, nil
}

return strconv.ParseUint(value, 10, 64)
}

// GetAll gets all values of a key
func (v GitConfig) GetAll(key string) []string {
section, key := toSectionKey(key)

keys := v[section]
if keys != nil {
return keys[key]
}
return nil
}

func dequoteKey(key string) string {
if !strings.ContainsAny(key, "\"'") {
return key
}

keys := []string{}
for _, k := range strings.Split(key, ".") {
keys = append(keys, strings.Trim(k, "\"'"))

}
return strings.Join(keys, ".")
}

// splitKey will split git config variable to section name and key
func toSectionKey(name string) (string, string) {
name = strings.ToLower(dequoteKey(name))
items := strings.Split(name, ".")

if len(items) < 2 {
return "", ""
}
key := items[len(items)-1]
section := strings.Join(items[0:len(items)-1], ".")
return section, key
}
@@ -0,0 +1,161 @@
package goconfig

import (
"testing"

"github.com/stretchr/testify/assert"
)

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

data := `# The following section name should have quote, like: [a "b"]
[a b]
c = d`
_, lineno, err := Parse([]byte(data))
assert.Equal(ErrMissingStartQuote, err)
assert.Equal(uint(2), lineno)
}

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

data := `# keys should not have spaces
[a]
b c = d`
_, lineno, err := Parse([]byte(data))
assert.Equal(ErrInvalidKeyChar, err)
assert.Equal(uint(3), lineno)
}

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

data := `[ab "cd"]
value1 = x
value2 = x y
value3 = a \"quote
[remote "hello world"]
url = test`
cfg, _, err := Parse([]byte(data))
assert.Nil(err)
assert.Equal("x", cfg.Get("ab.cd.value1"))
assert.Equal("x y", cfg.Get("ab.cd.value2"))
assert.Equal("a \"quote", cfg.Get("ab.cd.value3"))
}

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

data := `[remote "hello world"]
url = test`
cfg, _, err := Parse([]byte(data))
assert.Nil(err)
assert.Equal("test", cfg.Get("remote.hello world.url"))
assert.Equal("test", cfg.Get(`remote."hello world".url`))
assert.Equal("test", cfg.Get(`"remote.hello world".url`))
assert.Equal("test", cfg.Get(`"remote.hello world.url"`))
}

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

data := `[remote "origin"]
url = https://example.com/my/repo.git
fetch = +refs/heads/*:refs/remotes/origin/*
fetch = +refs/tags/*:refs/tags/*`
cfg, _, err := Parse([]byte(data))
assert.Nil(err)
assert.Equal("+refs/tags/*:refs/tags/*", cfg.Get("remote.origin.fetch"))
assert.Equal([]string{
"+refs/heads/*:refs/remotes/origin/*",
"+refs/tags/*:refs/tags/*",
}, cfg.GetAll("remote.origin.fetch"))

}

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

data := `[a]
t1 = true
t2 = yes
t3 = on
f1 = false
f2 = no
f3 = off
x1 = 1
x2 = nothing`

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

v, err := cfg.GetBool("a.t1", false)
assert.Nil(err)
assert.True(v)

v, err = cfg.GetBool("a.t2", false)
assert.Nil(err)
assert.True(v)

v, err = cfg.GetBool("a.t3", false)
assert.Nil(err)
assert.True(v)

v, err = cfg.GetBool("a.t4", false)
assert.Nil(err)
assert.False(v)

v, err = cfg.GetBool("a.f1", true)
assert.Nil(err)
assert.False(v)

v, err = cfg.GetBool("a.f2", true)
assert.Nil(err)
assert.False(v)

v, err = cfg.GetBool("a.f3", true)
assert.Nil(err)
assert.False(v)

v, err = cfg.GetBool("a.f4", true)
assert.Nil(err)
assert.True(v)

v, err = cfg.GetBool("a.x1", true)
assert.Equal(ErrNotBoolValue, err)

v, err = cfg.GetBool("a.x2", true)
assert.Equal(ErrNotBoolValue, err)
}

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

data := `[a]
i1 = 1
i2 = 100
i3 = abc`

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

v1, err := cfg.GetInt("a.i1", 0)
assert.Nil(err)
assert.Equal(1, v1)

v2, err := cfg.GetInt64("a.i2", 0)
assert.Nil(err)
assert.Equal(int64(100), v2)

v3, err := cfg.GetUint64("a.i2", 0)
assert.Nil(err)
assert.Equal(uint64(100), v3)

_, err = cfg.GetInt("a.i3", 0)
assert.NotNil(err)

v4, err := cfg.GetInt("a.i4", 6700)
assert.Nil(err)
assert.Equal(6700, v4)
}
@@ -9,16 +9,16 @@ type parser struct {
}

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

func (cf *parser) parse() (map[string]string, error) {
func (cf *parser) parse() (GitConfig, error) {
bomPtr := 0
comment := false
cfg := map[string]string{}
cfg := NewGitConfig()
name := ""
var err error
for {
@@ -54,18 +54,17 @@ func (cf *parser) parse() (map[string]string, error) {
if err != nil {
return cfg, err
}
name += "."
continue
}
if !isalpha(c) {
return cfg, ErrInvalidKeyChar
}
key := name + string(lower(c))
key := string(lower(c))
value, err := cf.getValue(&key)
if err != nil {
return cfg, err
}
cfg[key] = value
cfg._add(name, key, value)
}
}

Oops, something went wrong.

0 comments on commit 9e83c31

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