aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--formats/config/common.go96
-rw-r--r--formats/config/common_test.go86
-rw-r--r--formats/config/decoder.go37
-rw-r--r--formats/config/decoder_test.go90
-rw-r--r--formats/config/doc.go200
-rw-r--r--formats/config/encoder.go38
-rw-r--r--formats/config/encoder_test.go21
-rw-r--r--formats/config/fixtures_test.go90
-rw-r--r--formats/config/option.go83
-rw-r--r--formats/config/option_test.go33
-rw-r--r--formats/config/section.go76
-rw-r--r--formats/config/section_test.go71
-rw-r--r--storage/filesystem/config.go103
-rw-r--r--storage/filesystem/config_test.go47
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/*
-`)