diff options
-rw-r--r-- | formats/config/common.go | 96 | ||||
-rw-r--r-- | formats/config/common_test.go | 86 | ||||
-rw-r--r-- | formats/config/decoder.go | 37 | ||||
-rw-r--r-- | formats/config/decoder_test.go | 90 | ||||
-rw-r--r-- | formats/config/doc.go | 200 | ||||
-rw-r--r-- | formats/config/encoder.go | 38 | ||||
-rw-r--r-- | formats/config/encoder_test.go | 21 | ||||
-rw-r--r-- | formats/config/fixtures_test.go | 90 | ||||
-rw-r--r-- | formats/config/option.go | 83 | ||||
-rw-r--r-- | formats/config/option_test.go | 33 | ||||
-rw-r--r-- | formats/config/section.go | 76 | ||||
-rw-r--r-- | formats/config/section_test.go | 71 | ||||
-rw-r--r-- | storage/filesystem/config.go | 103 | ||||
-rw-r--r-- | storage/filesystem/config_test.go | 47 |
14 files changed, 1017 insertions, 54 deletions
diff --git a/formats/config/common.go b/formats/config/common.go new file mode 100644 index 0000000..e7292e9 --- /dev/null +++ b/formats/config/common.go @@ -0,0 +1,96 @@ +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 + +const ( + NoSubsection = "" +) + +func (c *Config) Section(name string) *Section { + for i := len(c.Sections) - 1; i >= 0; i-- { + 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 { + 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 == "" { + 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 +} diff --git a/formats/config/common_test.go b/formats/config/common_test.go new file mode 100644 index 0000000..0bc4d2d --- /dev/null +++ b/formats/config/common_test.go @@ -0,0 +1,86 @@ +package config + +import ( + "testing" + + . "gopkg.in/check.v1" +) + +func Test(t *testing.T) { TestingT(t) } + +type CommonSuite struct{} + +var _ = Suite(&CommonSuite{}) + +func (s *CommonSuite) TestConfig_SetOption(c *C) { + obtained := New().SetOption("section", "", "key1", "value1") + expected := &Config{ + Sections: []*Section{ + { + Name: "section", + Options: []*Option{ + {Key: "key1", Value: "value1"}, + }, + }, + }, + } + c.Assert(obtained, DeepEquals, expected) + obtained = obtained.SetOption("section", "", "key1", "value1") + c.Assert(obtained, DeepEquals, expected) + + obtained = New().SetOption("section", "subsection", "key1", "value1") + expected = &Config{ + Sections: []*Section{ + { + Name: "section", + Subsections: []*Subsection{ + { + Name: "subsection", + Options: []*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 := New().AddOption("section", "", "key1", "value1") + expected := &Config{ + Sections: []*Section{ + { + Name: "section", + Options: []*Option{ + {Key: "key1", Value: "value1"}, + }, + }, + }, + } + c.Assert(obtained, DeepEquals, expected) +} + +func (s *CommonSuite) TestConfig_RemoveSection(c *C) { + sect := New(). + AddOption("section1", "", "key1", "value1"). + AddOption("section2", "", "key1", "value1") + expected := 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 := New(). + AddOption("section1", "sub1", "key1", "value1"). + AddOption("section1", "sub2", "key1", "value1") + expected := 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) +} diff --git a/formats/config/decoder.go b/formats/config/decoder.go new file mode 100644 index 0000000..0f02ce1 --- /dev/null +++ b/formats/config/decoder.go @@ -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) +} diff --git a/formats/config/decoder_test.go b/formats/config/decoder_test.go new file mode 100644 index 0000000..412549f --- /dev/null +++ b/formats/config/decoder_test.go @@ -0,0 +1,90 @@ +package config + +import ( + "bytes" + + . "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 := NewDecoder(r) + cfg := &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 := NewDecoder(r) + cfg := &Config{} + err := d.Decode(cfg) + c.Assert(err, NotNil) +} diff --git a/formats/config/doc.go b/formats/config/doc.go new file mode 100644 index 0000000..1f7eb78 --- /dev/null +++ b/formats/config/doc.go @@ -0,0 +1,200 @@ +// Package config implements decoding, encoding and manipulation +// of git config files. +package config + +/* + +CONFIGURATION FILE +------------------ + +The Git configuration file contains a number of variables that affect +the Git commands' behavior. The `.git/config` file in each repository +is used to store the configuration for that repository, and +`$HOME/.gitconfig` is used to store a per-user configuration as +fallback values for the `.git/config` file. The file `/etc/gitconfig` +can be used to store a system-wide default configuration. + +The configuration variables are used by both the Git plumbing +and the porcelains. The variables are divided into sections, wherein +the fully qualified variable name of the variable itself is the last +dot-separated segment and the section name is everything before the last +dot. The variable names are case-insensitive, allow only alphanumeric +characters and `-`, and must start with an alphabetic character. Some +variables may appear multiple times; we say then that the variable is +multivalued. + +Syntax +~~~~~~ + +The syntax is fairly flexible and permissive; whitespaces are mostly +ignored. The '#' and ';' characters begin comments to the end of line, +blank lines are ignored. + +The file consists of sections and variables. A section begins with +the name of the section in square brackets and continues until the next +section begins. Section names are case-insensitive. Only alphanumeric +characters, `-` and `.` are allowed in section names. Each variable +must belong to some section, which means that there must be a section +header before the first setting of a variable. + +Sections can be further divided into subsections. To begin a subsection +put its name in double quotes, separated by space from the section name, +in the section header, like in the example below: + +-------- + [section "subsection"] + +-------- + +Subsection names are case sensitive and can contain any characters except +newline (doublequote `"` and backslash can be included by escaping them +as `\"` and `\\`, respectively). Section headers cannot span multiple +lines. Variables may belong directly to a section or to a given subsection. +You can have `[section]` if you have `[section "subsection"]`, but you +don't need to. + +There is also a deprecated `[section.subsection]` syntax. With this +syntax, the subsection name is converted to lower-case and is also +compared case sensitively. These subsection names follow the same +restrictions as section names. + +All the other lines (and the remainder of the line after the section +header) are recognized as setting variables, in the form +'name = value' (or just 'name', which is a short-hand to say that +the variable is the boolean "true"). +The variable names are case-insensitive, allow only alphanumeric characters +and `-`, and must start with an alphabetic character. + +A line that defines a value can be continued to the next line by +ending it with a `\`; the backquote and the end-of-line are +stripped. Leading whitespaces after 'name =', the remainder of the +line after the first comment character '#' or ';', and trailing +whitespaces of the line are discarded unless they are enclosed in +double quotes. Internal whitespaces within the value are retained +verbatim. + +Inside double quotes, double quote `"` and backslash `\` characters +must be escaped: use `\"` for `"` and `\\` for `\`. + +The following escape sequences (beside `\"` and `\\`) are recognized: +`\n` for newline character (NL), `\t` for horizontal tabulation (HT, TAB) +and `\b` for backspace (BS). Other char escape sequences (including octal +escape sequences) are invalid. + + +Includes +~~~~~~~~ + +You can include one config file from another by setting the special +`include.path` variable to the name of the file to be included. The +variable takes a pathname as its value, and is subject to tilde +expansion. + +The +included file is expanded immediately, as if its contents had been +found at the location of the include directive. If the value of the +`include.path` variable is a relative path, the path is considered to be +relative to the configuration file in which the include directive was +found. See below for examples. + + +Example +~~~~~~~ + + # Core variables + [core] + ; Don't trust file modes + filemode = false + + # Our diff algorithm + [diff] + external = /usr/local/bin/diff-wrapper + renames = true + + [branch "devel"] + remote = origin + merge = refs/heads/devel + + # Proxy settings + [core] + gitProxy="ssh" for "kernel.org" + gitProxy=default-proxy ; for the rest + + [include] + path = /path/to/foo.inc ; include by absolute path + path = foo ; expand "foo" relative to the current file + path = ~/foo ; expand "foo" in your `$HOME` directory + + +Values +~~~~~~ + +Values of many variables are treated as a simple string, but there +are variables that take values of specific types and there are rules +as to how to spell them. + +boolean:: + + When a variable is said to take a boolean value, many + synonyms are accepted for 'true' and 'false'; these are all + case-insensitive. + + true;; Boolean true can be spelled as `yes`, `on`, `true`, + or `1`. Also, a variable defined without `= <value>` + is taken as true. + + false;; Boolean false can be spelled as `no`, `off`, + `false`, or `0`. ++ +When converting value to the canonical form using `--bool` type +specifier; 'git config' will ensure that the output is "true" or +"false" (spelled in lowercase). + +integer:: + The value for many variables that specify various sizes can + be suffixed with `k`, `M`,... to mean "scale the number by + 1024", "by 1024x1024", etc. + +color:: + The value for a variable that takes a color is a list of + colors (at most two, one for foreground and one for background) + and attributes (as many as you want), separated by spaces. ++ +The basic colors accepted are `normal`, `black`, `red`, `green`, `yellow`, +`blue`, `magenta`, `cyan` and `white`. The first color given is the +foreground; the second is the background. ++ +Colors may also be given as numbers between 0 and 255; these use ANSI +256-color mode (but note that not all terminals may support this). If +your terminal supports it, you may also specify 24-bit RGB values as +hex, like `#ff0ab3`. ++ + +From: https://git-scm.com/docs/git-config +The accepted attributes are `bold`, `dim`, `ul`, `blink`, `reverse`, +`italic`, and `strike` (for crossed-out or "strikethrough" letters). +The position of any attributes with respect to the colors +(before, after, or in between), doesn't matter. Specific attributes may +be turned off by prefixing them with `no` or `no-` (e.g., `noreverse`, +`no-ul`, etc). ++ +For git's pre-defined color slots, the attributes are meant to be reset +at the beginning of each item in the colored output. So setting +`color.decorate.branch` to `black` will paint that branch name in a +plain `black`, even if the previous thing on the same output line (e.g. +opening parenthesis before the list of branch names in `log --decorate` +output) is set to be painted with `bold` or some other attribute. +However, custom log formats may do more complicated and layered +coloring, and the negated forms may be useful there. + +pathname:: + A variable that takes a pathname value can be given a + string that begins with "`~/`" or "`~user/`", and the usual + tilde expansion happens to such a string: `~/` + is expanded to the value of `$HOME`, and `~user/` to the + specified user's home directory. + +From: +https://raw.githubusercontent.com/git/git/659889482ac63411daea38b2c3d127842ea04e4d/Documentation/config.txt + +*/ diff --git a/formats/config/encoder.go b/formats/config/encoder.go new file mode 100644 index 0000000..4a68e44 --- /dev/null +++ b/formats/config/encoder.go @@ -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 +} diff --git a/formats/config/encoder_test.go b/formats/config/encoder_test.go new file mode 100644 index 0000000..5335b83 --- /dev/null +++ b/formats/config/encoder_test.go @@ -0,0 +1,21 @@ +package config + +import ( + "bytes" + + . "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 := 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)) + } +} diff --git a/formats/config/fixtures_test.go b/formats/config/fixtures_test.go new file mode 100644 index 0000000..12ff288 --- /dev/null +++ b/formats/config/fixtures_test.go @@ -0,0 +1,90 @@ +package config + +type Fixture struct { + Text string + Raw string + Config *Config +} + +var fixtures = []*Fixture{ + { + Raw: "", + Text: "", + Config: New(), + }, + { + Raw: ";Comments only", + Text: "", + Config: New(), + }, + { + Raw: "#Comments only", + Text: "", + Config: New(), + }, + { + Raw: "[core]\nrepositoryformatversion=0", + Text: "[core]\n\trepositoryformatversion = 0\n", + Config: New().AddOption("core", "", "repositoryformatversion", "0"), + }, + { + Raw: "[core]\n\trepositoryformatversion = 0\n", + Text: "[core]\n\trepositoryformatversion = 0\n", + Config: New().AddOption("core", "", "repositoryformatversion", "0"), + }, + { + Raw: ";Commment\n[core]\n;Comment\nrepositoryformatversion = 0\n", + Text: "[core]\n\trepositoryformatversion = 0\n", + Config: New().AddOption("core", "", "repositoryformatversion", "0"), + }, + { + Raw: "#Commment\n#Comment\n[core]\n#Comment\nrepositoryformatversion = 0\n", + Text: "[core]\n\trepositoryformatversion = 0\n", + Config: New().AddOption("core", "", "repositoryformatversion", "0"), + }, + { + Raw: ` + [sect1] + opt1 = value1 + [sect1 "subsect1"] + opt2 = value2 + `, + Text: `[sect1] + opt1 = value1 +[sect1 "subsect1"] + opt2 = value2 +`, + Config: New(). + AddOption("sect1", "", "opt1", "value1"). + AddOption("sect1", "subsect1", "opt2", "value2"), + }, + { + Raw: ` + [sect1] + opt1 = value1 + [sect1 "subsect1"] + opt2 = value2 + [sect1] + opt1 = value1b + [sect1 "subsect1"] + opt2 = value2b + [sect1 "subsect2"] + opt2 = value2 + `, + Text: `[sect1] + opt1 = value1 + opt1 = value1b +[sect1 "subsect1"] + opt2 = value2 + opt2 = value2b +[sect1 "subsect2"] + opt2 = value2 +`, + Config: New(). + AddOption("sect1", "", "opt1", "value1"). + AddOption("sect1", "", "opt1", "value1b"). + AddOption("sect1", "subsect1", "opt2", "value2"). + AddOption("sect1", "subsect1", "opt2", "value2b"). + AddOption("sect1", "subsect2", "opt2", "value2"), + }, +} diff --git a/formats/config/option.go b/formats/config/option.go new file mode 100644 index 0000000..dbb401c --- /dev/null +++ b/formats/config/option.go @@ -0,0 +1,83 @@ +package config + +import ( + "strings" +) + +type Option struct { + // Key preserving original caseness. + // Use IsKey instead to compare key regardless of caseness. + Key string + // Original value as string, could be not notmalized. + Value string +} + +type Options []*Option + +// IsKey returns true if the given key matches +// this options' key in a case-insensitive comparison. +func (o *Option) IsKey(key string) bool { + return strings.ToLower(o.Key) == strings.ToLower(key) +} + +// Get gets the value for the given key if set, +// otherwise it returns the empty string. +// +// Note that there is no difference +// +// This matches git behaviour since git v1.8.1-rc1, +// if there are multiple definitions of a key, the +// last one wins. +// +// See: http://article.gmane.org/gmane.linux.kernel/1407184 +// +// In order to get all possible values for the same key, +// use GetAll. +func (opts Options) Get(key string) string { + for i := len(opts) - 1; i >= 0; i-- { + o := opts[i] + if o.IsKey(key) { + return o.Value + } + } + return "" +} + +// GetAll returns all possible values for the same key. +func (opts Options) GetAll(key string) []string { + result := []string{} + for _, o := range opts { + if o.IsKey(key) { + result = append(result, o.Value) + } + } + return result +} + +func (opts Options) withoutOption(key string) Options { + result := Options{} + for _, o := range opts { + if !o.IsKey(key) { + result = append(result, o) + } + } + return result +} + +func (opts Options) withAddedOption(key string, value string) Options { + return append(opts, &Option{key, value}) +} + +func (opts Options) withSettedOption(key string, value string) Options { + for i := len(opts) - 1; i >= 0; i-- { + o := opts[i] + if o.IsKey(key) { + result := make(Options, len(opts)) + copy(result, opts) + result[i] = &Option{key, value} + return result + } + } + + return opts.withAddedOption(key, value) +} diff --git a/formats/config/option_test.go b/formats/config/option_test.go new file mode 100644 index 0000000..8588de1 --- /dev/null +++ b/formats/config/option_test.go @@ -0,0 +1,33 @@ +package config + +import ( + . "gopkg.in/check.v1" +) + +type OptionSuite struct{} + +var _ = Suite(&OptionSuite{}) + +func (s *OptionSuite) TestOptions_GetAll(c *C) { + o := Options{ + &Option{"k", "v"}, + &Option{"ok", "v1"}, + &Option{"K", "v2"}, + } + c.Assert(o.GetAll("k"), DeepEquals, []string{"v", "v2"}) + c.Assert(o.GetAll("K"), DeepEquals, []string{"v", "v2"}) + c.Assert(o.GetAll("ok"), DeepEquals, []string{"v1"}) + c.Assert(o.GetAll("unexistant"), DeepEquals, []string{}) + + o = Options{} + c.Assert(o.GetAll("k"), DeepEquals, []string{}) +} + +func (s *OptionSuite) TestOption_IsKey(c *C) { + c.Assert((&Option{Key: "key"}).IsKey("key"), Equals, true) + c.Assert((&Option{Key: "key"}).IsKey("KEY"), Equals, true) + c.Assert((&Option{Key: "KEY"}).IsKey("key"), Equals, true) + c.Assert((&Option{Key: "key"}).IsKey("other"), Equals, false) + c.Assert((&Option{Key: "key"}).IsKey(""), Equals, false) + c.Assert((&Option{Key: ""}).IsKey("key"), Equals, false) +} diff --git a/formats/config/section.go b/formats/config/section.go new file mode 100644 index 0000000..6d6a891 --- /dev/null +++ b/formats/config/section.go @@ -0,0 +1,76 @@ +package config + +import "strings" + +type Section struct { + Name string + Options Options + Subsections Subsections +} + +type Subsection struct { + Name string + Options Options +} + +type Sections []*Section + +type Subsections []*Subsection + +func (s *Section) IsName(name string) bool { + return strings.ToLower(s.Name) == strings.ToLower(name) +} + +func (s *Subsection) IsName(name string) bool { + return s.Name == name +} + +func (s *Section) Option(key string) string { + return s.Options.Get(key) +} + +func (s *Subsection) Option(key string) string { + return s.Options.Get(key) +} + +func (s *Section) AddOption(key string, value string) *Section { + s.Options = s.Options.withAddedOption(key, value) + return s +} + +func (s *Subsection) AddOption(key string, value string) *Subsection { + s.Options = s.Options.withAddedOption(key, value) + return s +} + +func (s *Section) SetOption(key string, value string) *Section { + s.Options = s.Options.withSettedOption(key, value) + return s +} + +func (s *Section) RemoveOption(key string) *Section { + s.Options = s.Options.withoutOption(key) + return s +} + +func (s *Subsection) SetOption(key string, value string) *Subsection { + s.Options = s.Options.withSettedOption(key, value) + return s +} + +func (s *Subsection) RemoveOption(key string) *Subsection { + s.Options = s.Options.withoutOption(key) + return s +} + +func (s *Section) Subsection(name string) *Subsection { + for i := len(s.Subsections) - 1; i >= 0; i-- { + ss := s.Subsections[i] + if ss.IsName(name) { + return ss + } + } + ss := &Subsection{Name: name} + s.Subsections = append(s.Subsections, ss) + return ss +} diff --git a/formats/config/section_test.go b/formats/config/section_test.go new file mode 100644 index 0000000..cfd9f3f --- /dev/null +++ b/formats/config/section_test.go @@ -0,0 +1,71 @@ +package config + +import ( + . "gopkg.in/check.v1" +) + +type SectionSuite struct{} + +var _ = Suite(&SectionSuite{}) + +func (s *SectionSuite) TestSection_Option(c *C) { + sect := &Section{ + Options: []*Option{ + {Key: "key1", Value: "value1"}, + {Key: "key2", Value: "value2"}, + {Key: "key1", Value: "value3"}, + }, + } + c.Assert(sect.Option("otherkey"), Equals, "") + c.Assert(sect.Option("key2"), Equals, "value2") + c.Assert(sect.Option("key1"), Equals, "value3") +} + +func (s *SectionSuite) TestSubsection_Option(c *C) { + sect := &Subsection{ + Options: []*Option{ + {Key: "key1", Value: "value1"}, + {Key: "key2", Value: "value2"}, + {Key: "key1", Value: "value3"}, + }, + } + c.Assert(sect.Option("otherkey"), Equals, "") + c.Assert(sect.Option("key2"), Equals, "value2") + c.Assert(sect.Option("key1"), Equals, "value3") +} + +func (s *SectionSuite) TestSection_RemoveOption(c *C) { + sect := &Section{ + Options: []*Option{ + {Key: "key1", Value: "value1"}, + {Key: "key2", Value: "value2"}, + {Key: "key1", Value: "value3"}, + }, + } + c.Assert(sect.RemoveOption("otherkey"), DeepEquals, sect) + + expected := &Section{ + Options: []*Option{ + {Key: "key2", Value: "value2"}, + }, + } + c.Assert(sect.RemoveOption("key1"), DeepEquals, expected) +} + +func (s *SectionSuite) TestSubsection_RemoveOption(c *C) { + sect := &Subsection{ + Options: []*Option{ + {Key: "key1", Value: "value1"}, + {Key: "key2", Value: "value2"}, + {Key: "key1", Value: "value3"}, + }, + } + c.Assert(sect.RemoveOption("otherkey"), DeepEquals, sect) + + expected := &Subsection{ + Options: []*Option{ + {Key: "key2", Value: "value2"}, + }, + } + c.Assert(sect.RemoveOption("key1"), DeepEquals, expected) +} diff --git a/storage/filesystem/config.go b/storage/filesystem/config.go index 1cff05a..84252eb 100644 --- a/storage/filesystem/config.go +++ b/storage/filesystem/config.go @@ -1,58 +1,79 @@ package filesystem import ( - "fmt" - "io" - - "gopkg.in/gcfg.v1" "gopkg.in/src-d/go-git.v4/config" + gitconfig "gopkg.in/src-d/go-git.v4/formats/config" "gopkg.in/src-d/go-git.v4/storage/filesystem/internal/dotgit" ) +const ( + remoteSection = "remote" + fetchKey = "fetch" + urlKey = "url" +) + type ConfigStorage struct { dir *dotgit.DotGit } func (c *ConfigStorage) Remote(name string) (*config.RemoteConfig, error) { - file, err := c.read() + cfg, err := c.read() if err != nil { return nil, err } - r, ok := file.Remotes[name] - if ok { - return r, nil + s := cfg.Section(remoteSection).Subsection(name) + if s == nil { + return nil, config.ErrRemoteConfigNotFound } - return nil, config.ErrRemoteConfigNotFound + return parseRemote(s), nil } func (c *ConfigStorage) Remotes() ([]*config.RemoteConfig, error) { - file, err := c.read() + cfg, err := c.read() if err != nil { return nil, err } - remotes := make([]*config.RemoteConfig, len(file.Remotes)) - - var i int - for _, r := range file.Remotes { - remotes[i] = r + remotes := []*config.RemoteConfig{} + sect := cfg.Section(remoteSection) + for _, s := range sect.Subsections { + remotes = append(remotes, parseRemote(s)) } return remotes, nil } func (c *ConfigStorage) SetRemote(r *config.RemoteConfig) error { - return nil - return fmt.Errorf("set remote - not implemented yet") + cfg, err := c.read() + if err != nil { + return err + } + + s := cfg.Section(remoteSection).Subsection(r.Name) + s.Name = r.Name + s.SetOption(urlKey, r.URL) + s.RemoveOption(fetchKey) + for _, rs := range r.Fetch { + s.AddOption(fetchKey, rs.String()) + } + + return c.write(cfg) } func (c *ConfigStorage) DeleteRemote(name string) error { - return fmt.Errorf("delete - remote not implemented yet") + cfg, err := c.read() + if err != nil { + return err + } + + cfg = cfg.RemoveSubsection(remoteSection, name) + + return c.write(cfg) } -func (c *ConfigStorage) read() (*ConfigFile, error) { +func (c *ConfigStorage) read() (*gitconfig.Config, error) { f, err := c.dir.Config() if err != nil { return nil, err @@ -60,15 +81,45 @@ func (c *ConfigStorage) read() (*ConfigFile, error) { defer f.Close() - config := &ConfigFile{} - return config, config.Decode(f) + cfg := gitconfig.New() + d := gitconfig.NewDecoder(f) + err = d.Decode(cfg) + if err != nil { + return nil, err + } + + return cfg, nil } -type ConfigFile struct { - Remotes map[string]*config.RemoteConfig `gcfg:"remote"` +func (c *ConfigStorage) write(cfg *gitconfig.Config) error { + f, err := c.dir.Config() + if err != nil { + return err + } + + defer f.Close() + + e := gitconfig.NewEncoder(f) + err = e.Encode(cfg) + if err != nil { + return err + } + + return nil } -// Decode decode a git config file intro the ConfigStore -func (c *ConfigFile) Decode(r io.Reader) error { - return gcfg.FatalOnly(gcfg.ReadInto(c, r)) +func parseRemote(s *gitconfig.Subsection) *config.RemoteConfig { + fetch := []config.RefSpec{} + for _, f := range s.Options.GetAll(fetchKey) { + rs := config.RefSpec(f) + if rs.IsValid() { + fetch = append(fetch, rs) + } + } + + return &config.RemoteConfig{ + Name: s.Name, + URL: s.Option(urlKey), + Fetch: fetch, + } } diff --git a/storage/filesystem/config_test.go b/storage/filesystem/config_test.go index cbff1e0..20af595 100644 --- a/storage/filesystem/config_test.go +++ b/storage/filesystem/config_test.go @@ -1,7 +1,7 @@ package filesystem import ( - "bytes" + "gopkg.in/src-d/go-git.v4/formats/config" . "gopkg.in/check.v1" ) @@ -10,31 +10,22 @@ type ConfigSuite struct{} var _ = Suite(&ConfigSuite{}) -func (s *ConfigSuite) TestConfigFileDecode(c *C) { - config := &ConfigFile{} - - err := config.Decode(bytes.NewBuffer(configFixture)) - c.Assert(err, IsNil) - - c.Assert(config.Remotes, HasLen, 2) - c.Assert(config.Remotes["origin"].URL, Equals, "git@github.com:src-d/go-git.git") - c.Assert(config.Remotes["origin"].Fetch, HasLen, 1) - c.Assert(config.Remotes["origin"].Fetch[0].String(), Equals, "+refs/heads/*:refs/remotes/origin/*") +func (s *ConfigSuite) TestParseRemote(c *C) { + remote := parseRemote(&config.Subsection{ + Name: "origin", + Options: []*config.Option{ + { + Key: "url", + Value: "git@github.com:src-d/go-git.git", + }, + { + Key: "fetch", + Value: "+refs/heads/*:refs/remotes/origin/*", + }, + }, + }) + + c.Assert(remote.URL, Equals, "git@github.com:src-d/go-git.git") + c.Assert(remote.Fetch, HasLen, 1) + c.Assert(remote.Fetch[0].String(), Equals, "+refs/heads/*:refs/remotes/origin/*") } - -var configFixture = []byte(` -[core] - repositoryformatversion = 0 - filemode = true - bare = false - logallrefupdates = true -[remote "origin"] - url = git@github.com:src-d/go-git.git - fetch = +refs/heads/*:refs/remotes/origin/* -[branch "v4"] - remote = origin - merge = refs/heads/v4 -[remote "mcuadros"] - url = git@github.com:mcuadros/go-git.git - fetch = +refs/heads/*:refs/remotes/mcuadros/* -`) |