Skip to content
This repository was archived by the owner on Sep 11, 2020. It is now read-only.

formats/config: Added encoder/decoder for git config files. #97

Merged
merged 14 commits into from
Oct 26, 2016
Merged
Show file tree
Hide file tree
Changes from 10 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
92 changes: 92 additions & 0 deletions formats/config/common.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
// Package config implements decoding, encoding and
Copy link
Contributor

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.go

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done.

// 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
Copy link
Contributor

Choose a reason for hiding this comment

The 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?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The 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 (;. #).

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ok 👍


func (c *Config) Section(name string) *Section {
for i := len(c.Sections) - 1; i >= 0; i-- {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why not you prefer range ? Is there any special meaning iterating in reverse order ?

Copy link
Contributor

Choose a reason for hiding this comment

The 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?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The 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).

Copy link
Contributor

Choose a reason for hiding this comment

The 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.
// If subsection is empty, then it's taken as no subsection.
func (s *Config) AddOption(section string, subsection string, key string, value string) *Config {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Does this method returns its receiver to be chainable?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Right.

Copy link
Contributor

Choose a reason for hiding this comment

The 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.
// If subsection is empty, then it's taken as no subsection.
func (s *Config) SetOption(section string, subsection string, key string, value string) *Config {
if subsection == "" {
Copy link
Contributor

Choose a reason for hiding this comment

The 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 NoSection constant with the value of ""?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The 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
}
89 changes: 89 additions & 0 deletions formats/config/common_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
package config_test
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I prefer not having _test packages, and using the same package

Copy link
Collaborator Author

Choose a reason for hiding this comment

The 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 {
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In the same link of the open brace

Copy link
Collaborator Author

Choose a reason for hiding this comment

The 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)
}
37 changes: 37 additions & 0 deletions formats/config/decoder.go
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)
}
93 changes: 93 additions & 0 deletions formats/config/decoder_test.go
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)
}
38 changes: 38 additions & 0 deletions formats/config/encoder.go
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
}
24 changes: 24 additions & 0 deletions formats/config/encoder_test.go
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))
}
}
Loading