-
Notifications
You must be signed in to change notification settings - Fork 534
formats/config: Added encoder/decoder for git config files. #97
Changes from 11 commits
2517001
1a101f0
e83c095
c953939
0850356
a47cc39
6620dfc
8bc9d3e
8e203f0
668198f
bce963f
270d40e
935a6eb
ca8b401
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,100 @@ | ||
// Package config implements decoding, encoding and | ||
// manipulation git config files. | ||
// | ||
// Reference: https://git-scm.com/docs/git-config | ||
package config | ||
|
||
// New creates a new config instance. | ||
func New() *Config { | ||
return &Config{} | ||
} | ||
|
||
type Config struct { | ||
Comment *Comment | ||
Sections Sections | ||
Includes Includes | ||
} | ||
|
||
type Includes []*Include | ||
|
||
// A reference to a included configuration. | ||
type Include struct { | ||
Path string | ||
Config *Config | ||
} | ||
|
||
type Comment string | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This is weird, did you forgot to add some methods to this type? otherwise, why can we use the string type instead of Comment? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Yep. It's still missing comment handling code such as returning comment without comment symbols (;. #). There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. ok 👍 |
||
|
||
const ( | ||
NoSubsection = "" | ||
) | ||
|
||
func (c *Config) Section(name string) *Section { | ||
for i := len(c.Sections) - 1; i >= 0; i-- { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Why not you prefer There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Does it has to do with disambiguating sections with the same name to the one that was appended last?, is name supposed to be an unique id? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. When we have multiple options with the same key, the standard git config behaviour for Get is that the last value wins. So we iterate in reverse order and return the first value. For Sections/Subsections we do the same. Although this will probably have to be changed in the future if we want to handle merging of repeated sections. At the moment, we do not require such functionality, but we'll have to handle this when we want to work with git config files in the wild (at the moment, we just want to get/set/delete remotes for our storage backend). There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 👍 |
||
s := c.Sections[i] | ||
if s.IsName(name) { | ||
return s | ||
} | ||
} | ||
s := &Section{Name: name} | ||
c.Sections = append(c.Sections, s) | ||
return s | ||
} | ||
|
||
// AddOption is a convenience method to add an option to a given | ||
// section and subsection. | ||
// | ||
// Use the NoSubsection constant for the subsection argument | ||
// if no subsection is wanted. | ||
func (s *Config) AddOption(section string, subsection string, key string, value string) *Config { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Does this method returns its receiver to be chainable? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Right. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 👍 |
||
if subsection == "" { | ||
s.Section(section).AddOption(key, value) | ||
} else { | ||
s.Section(section).Subsection(subsection).AddOption(key, value) | ||
} | ||
|
||
return s | ||
} | ||
|
||
// SetOption is a convenience method to set an option to a given | ||
// section and subsection. | ||
// | ||
// Use the NoSubsection constant for the subsection argument | ||
// if no subsection is wanted. | ||
func (s *Config) SetOption(section string, subsection string, key string, value string) *Config { | ||
if subsection == "" { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This is similar to what happen with flush-pkt in the past, that we should pass a constant instead of making the user aware of the specific value. How about a There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Done. |
||
s.Section(section).SetOption(key, value) | ||
} else { | ||
s.Section(section).Subsection(subsection).SetOption(key, value) | ||
} | ||
|
||
return s | ||
} | ||
|
||
func (c *Config) RemoveSection(name string) *Config { | ||
result := Sections{} | ||
for _, s := range c.Sections { | ||
if !s.IsName(name) { | ||
result = append(result, s) | ||
} | ||
} | ||
|
||
c.Sections = result | ||
return c | ||
} | ||
|
||
func (c *Config) RemoveSubsection(section string, subsection string) *Config { | ||
for _, s := range c.Sections { | ||
if s.IsName(section) { | ||
result := Subsections{} | ||
for _, ss := range s.Subsections { | ||
if !ss.IsName(subsection) { | ||
result = append(result, ss) | ||
} | ||
} | ||
s.Subsections = result | ||
} | ||
} | ||
|
||
return c | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,89 @@ | ||
package config_test | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I prefer not having _test packages, and using the same package There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Done. |
||
|
||
import ( | ||
"testing" | ||
|
||
"gopkg.in/src-d/go-git.v4/formats/config" | ||
|
||
. "gopkg.in/check.v1" | ||
) | ||
|
||
func Test(t *testing.T) { TestingT(t) } | ||
|
||
type CommonSuite struct { | ||
} | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. In the same link of the open brace There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Done. |
||
|
||
var _ = Suite(&CommonSuite{}) | ||
|
||
func (s *CommonSuite) TestConfig_SetOption(c *C) { | ||
obtained := config.New().SetOption("section", "", "key1", "value1") | ||
expected := &config.Config{ | ||
Sections: []*config.Section{ | ||
{ | ||
Name: "section", | ||
Options: []*config.Option{ | ||
{Key: "key1", Value: "value1"}, | ||
}, | ||
}, | ||
}, | ||
} | ||
c.Assert(obtained, DeepEquals, expected) | ||
obtained = obtained.SetOption("section", "", "key1", "value1") | ||
c.Assert(obtained, DeepEquals, expected) | ||
|
||
obtained = config.New().SetOption("section", "subsection", "key1", "value1") | ||
expected = &config.Config{ | ||
Sections: []*config.Section{ | ||
{ | ||
Name: "section", | ||
Subsections: []*config.Subsection{ | ||
{ | ||
Name: "subsection", | ||
Options: []*config.Option{ | ||
{Key: "key1", Value: "value1"}, | ||
}, | ||
}, | ||
}, | ||
}, | ||
}, | ||
} | ||
c.Assert(obtained, DeepEquals, expected) | ||
obtained = obtained.SetOption("section", "subsection", "key1", "value1") | ||
c.Assert(obtained, DeepEquals, expected) | ||
} | ||
|
||
func (s *CommonSuite) TestConfig_AddOption(c *C) { | ||
obtained := config.New().AddOption("section", "", "key1", "value1") | ||
expected := &config.Config{ | ||
Sections: []*config.Section{ | ||
{ | ||
Name: "section", | ||
Options: []*config.Option{ | ||
{Key: "key1", Value: "value1"}, | ||
}, | ||
}, | ||
}, | ||
} | ||
c.Assert(obtained, DeepEquals, expected) | ||
} | ||
|
||
func (s *CommonSuite) TestConfig_RemoveSection(c *C) { | ||
sect := config.New(). | ||
AddOption("section1", "", "key1", "value1"). | ||
AddOption("section2", "", "key1", "value1") | ||
expected := config.New(). | ||
AddOption("section1", "", "key1", "value1") | ||
c.Assert(sect.RemoveSection("other"), DeepEquals, sect) | ||
c.Assert(sect.RemoveSection("section2"), DeepEquals, expected) | ||
} | ||
|
||
func (s *CommonSuite) TestConfig_RemoveSubsection(c *C) { | ||
sect := config.New(). | ||
AddOption("section1", "sub1", "key1", "value1"). | ||
AddOption("section1", "sub2", "key1", "value1") | ||
expected := config.New(). | ||
AddOption("section1", "sub1", "key1", "value1") | ||
c.Assert(sect.RemoveSubsection("section1", "other"), DeepEquals, sect) | ||
c.Assert(sect.RemoveSubsection("other", "other"), DeepEquals, sect) | ||
c.Assert(sect.RemoveSubsection("section1", "sub2"), DeepEquals, expected) | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,37 @@ | ||
package config | ||
|
||
import ( | ||
"io" | ||
|
||
"github.com/src-d/gcfg" | ||
) | ||
|
||
// A Decoder reads and decodes config files from an input stream. | ||
type Decoder struct { | ||
io.Reader | ||
} | ||
|
||
// NewDecoder returns a new decoder that reads from r. | ||
func NewDecoder(r io.Reader) *Decoder { | ||
return &Decoder{r} | ||
} | ||
|
||
// Decode reads the whole config from its input and stores it in the | ||
// value pointed to by config. | ||
func (d *Decoder) Decode(config *Config) error { | ||
cb := func(s string, ss string, k string, v string, bv bool) error { | ||
if ss == "" && k == "" { | ||
config.Section(s) | ||
return nil | ||
} | ||
|
||
if ss != "" && k == "" { | ||
config.Section(s).Subsection(ss) | ||
return nil | ||
} | ||
|
||
config.AddOption(s, ss, k, v) | ||
return nil | ||
} | ||
return gcfg.ReadWithCallback(d, cb) | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,93 @@ | ||
package config_test | ||
|
||
import ( | ||
"bytes" | ||
|
||
"gopkg.in/src-d/go-git.v4/formats/config" | ||
|
||
. "gopkg.in/check.v1" | ||
) | ||
|
||
type DecoderSuite struct { | ||
} | ||
|
||
var _ = Suite(&DecoderSuite{}) | ||
|
||
func (s *DecoderSuite) TestDecode(c *C) { | ||
for idx, fixture := range fixtures { | ||
r := bytes.NewReader([]byte(fixture.Raw)) | ||
d := config.NewDecoder(r) | ||
cfg := &config.Config{} | ||
err := d.Decode(cfg) | ||
c.Assert(err, IsNil, Commentf("decoder error for fixture: %d", idx)) | ||
c.Assert(cfg, DeepEquals, fixture.Config, Commentf("bad result for fixture: %d", idx)) | ||
} | ||
} | ||
|
||
func (s *DecoderSuite) TestDecodeFailsWithIdentBeforeSection(c *C) { | ||
t := ` | ||
key=value | ||
[section] | ||
key=value | ||
` | ||
decodeFails(c, t) | ||
} | ||
|
||
func (s *DecoderSuite) TestDecodeFailsWithEmptySectionName(c *C) { | ||
t := ` | ||
[] | ||
key=value | ||
` | ||
decodeFails(c, t) | ||
} | ||
|
||
func (s *DecoderSuite) TestDecodeFailsWithEmptySubsectionName(c *C) { | ||
t := ` | ||
[remote ""] | ||
key=value | ||
` | ||
decodeFails(c, t) | ||
} | ||
|
||
func (s *DecoderSuite) TestDecodeFailsWithBadSubsectionName(c *C) { | ||
t := ` | ||
[remote origin"] | ||
key=value | ||
` | ||
decodeFails(c, t) | ||
t = ` | ||
[remote "origin] | ||
key=value | ||
` | ||
decodeFails(c, t) | ||
} | ||
|
||
func (s *DecoderSuite) TestDecodeFailsWithTrailingGarbage(c *C) { | ||
t := ` | ||
[remote]garbage | ||
key=value | ||
` | ||
decodeFails(c, t) | ||
t = ` | ||
[remote "origin"]garbage | ||
key=value | ||
` | ||
decodeFails(c, t) | ||
} | ||
|
||
func (s *DecoderSuite) TestDecodeFailsWithGarbage(c *C) { | ||
decodeFails(c, "---") | ||
decodeFails(c, "????") | ||
decodeFails(c, "[sect\nkey=value") | ||
decodeFails(c, "sect]\nkey=value") | ||
decodeFails(c, `[section]key="value`) | ||
decodeFails(c, `[section]key=value"`) | ||
} | ||
|
||
func decodeFails(c *C, text string) { | ||
r := bytes.NewReader([]byte(text)) | ||
d := config.NewDecoder(r) | ||
cfg := &config.Config{} | ||
err := d.Decode(cfg) | ||
c.Assert(err, NotNil) | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,38 @@ | ||
package config | ||
|
||
import ( | ||
"fmt" | ||
"io" | ||
) | ||
|
||
// An Encoder writes config files to an output stream. | ||
type Encoder struct { | ||
io.Writer | ||
} | ||
|
||
// NewEncoder returns a new encoder that writes to w. | ||
func NewEncoder(w io.Writer) *Encoder { | ||
return &Encoder{w} | ||
} | ||
|
||
// Encode writes the config in git config format to the stream of the encoder. | ||
func (e *Encoder) Encode(cfg *Config) error { | ||
for _, s := range cfg.Sections { | ||
if len(s.Options) > 0 { | ||
fmt.Fprintf(e, "[%s]\n", s.Name) | ||
for _, o := range s.Options { | ||
fmt.Fprintf(e, "\t%s = %s\n", o.Key, o.Value) | ||
} | ||
} | ||
for _, ss := range s.Subsections { | ||
if len(ss.Options) > 0 { | ||
//TODO: escape | ||
fmt.Fprintf(e, "[%s \"%s\"]\n", s.Name, ss.Name) | ||
for _, o := range ss.Options { | ||
fmt.Fprintf(e, "\t%s = %s\n", o.Key, o.Value) | ||
} | ||
} | ||
} | ||
} | ||
return nil | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,24 @@ | ||
package config_test | ||
|
||
import ( | ||
"bytes" | ||
|
||
"gopkg.in/src-d/go-git.v4/formats/config" | ||
|
||
. "gopkg.in/check.v1" | ||
) | ||
|
||
type EncoderSuite struct { | ||
} | ||
|
||
var _ = Suite(&EncoderSuite{}) | ||
|
||
func (s *EncoderSuite) TestEncode(c *C) { | ||
for idx, fixture := range fixtures { | ||
buf := &bytes.Buffer{} | ||
e := config.NewEncoder(buf) | ||
err := e.Encode(fixture.Config) | ||
c.Assert(err, IsNil, Commentf("encoder error for fixture: %d", idx)) | ||
c.Assert(buf.String(), Equals, fixture.Text, Commentf("bad result for fixture: %d", idx)) | ||
} | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The package doc description usually is filled in a
doc.go
and we copy the reference there: https://github.com/src-d/go-git/blob/master/formats/idxfile/doc.goThere was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Done.