diff options
author | Máximo Cuadros <mcuadros@gmail.com> | 2016-11-08 23:46:38 +0100 |
---|---|---|
committer | GitHub <noreply@github.com> | 2016-11-08 23:46:38 +0100 |
commit | ac095bb12c4d29722b60ba9f20590fa7cfa6bc7d (patch) | |
tree | 223f36f336ba3414b1e45cac8af6c4744a5d7ef6 /plumbing/format/config | |
parent | e523701393598f4fa241dd407af9ff8925507a1a (diff) | |
download | go-git-ac095bb12c4d29722b60ba9f20590fa7cfa6bc7d.tar.gz |
new plumbing package (#118)
* plumbing: now core was renamed to core, and formats and clients moved inside
Diffstat (limited to 'plumbing/format/config')
-rw-r--r-- | plumbing/format/config/common.go | 97 | ||||
-rw-r--r-- | plumbing/format/config/common_test.go | 86 | ||||
-rw-r--r-- | plumbing/format/config/decoder.go | 37 | ||||
-rw-r--r-- | plumbing/format/config/decoder_test.go | 90 | ||||
-rw-r--r-- | plumbing/format/config/doc.go | 199 | ||||
-rw-r--r-- | plumbing/format/config/encoder.go | 75 | ||||
-rw-r--r-- | plumbing/format/config/encoder_test.go | 21 | ||||
-rw-r--r-- | plumbing/format/config/fixtures_test.go | 90 | ||||
-rw-r--r-- | plumbing/format/config/option.go | 83 | ||||
-rw-r--r-- | plumbing/format/config/option_test.go | 33 | ||||
-rw-r--r-- | plumbing/format/config/section.go | 87 | ||||
-rw-r--r-- | plumbing/format/config/section_test.go | 71 |
12 files changed, 969 insertions, 0 deletions
diff --git a/plumbing/format/config/common.go b/plumbing/format/config/common.go new file mode 100644 index 0000000..d2f1e5c --- /dev/null +++ b/plumbing/format/config/common.go @@ -0,0 +1,97 @@ +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/plumbing/format/config/common_test.go b/plumbing/format/config/common_test.go new file mode 100644 index 0000000..365b53f --- /dev/null +++ b/plumbing/format/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", NoSubsection, "key1", "value1") + expected := &Config{ + Sections: []*Section{ + { + Name: "section", + Options: []*Option{ + {Key: "key1", Value: "value1"}, + }, + }, + }, + } + c.Assert(obtained, DeepEquals, expected) + obtained = obtained.SetOption("section", NoSubsection, "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", NoSubsection, "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", NoSubsection, "key1", "value1"). + AddOption("section2", NoSubsection, "key1", "value1") + expected := New(). + AddOption("section1", NoSubsection, "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/plumbing/format/config/decoder.go b/plumbing/format/config/decoder.go new file mode 100644 index 0000000..0f02ce1 --- /dev/null +++ b/plumbing/format/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/plumbing/format/config/decoder_test.go b/plumbing/format/config/decoder_test.go new file mode 100644 index 0000000..412549f --- /dev/null +++ b/plumbing/format/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/plumbing/format/config/doc.go b/plumbing/format/config/doc.go new file mode 100644 index 0000000..dd77fbc --- /dev/null +++ b/plumbing/format/config/doc.go @@ -0,0 +1,199 @@ +// Package config implements decoding/encoding 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/plumbing/format/config/encoder.go b/plumbing/format/config/encoder.go new file mode 100644 index 0000000..88bdf65 --- /dev/null +++ b/plumbing/format/config/encoder.go @@ -0,0 +1,75 @@ +package config + +import ( + "fmt" + "io" +) + +// An Encoder writes config files to an output stream. +type Encoder struct { + w 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 err := e.encodeSection(s); err != nil { + return err + } + } + + return nil +} + +func (e *Encoder) encodeSection(s *Section) error { + if len(s.Options) > 0 { + if err := e.printf("[%s]\n", s.Name); err != nil { + return err + } + + if err := e.encodeOptions(s.Options); err != nil { + return err + } + } + + for _, ss := range s.Subsections { + if err := e.encodeSubsection(s.Name, ss); err != nil { + return err + } + } + + return nil +} + +func (e *Encoder) encodeSubsection(sectionName string, s *Subsection) error { + //TODO: escape + if err := e.printf("[%s \"%s\"]\n", sectionName, s.Name); err != nil { + return err + } + + if err := e.encodeOptions(s.Options); err != nil { + return err + } + + return nil +} + +func (e *Encoder) encodeOptions(opts Options) error { + for _, o := range opts { + if err := e.printf("\t%s = %s\n", o.Key, o.Value); err != nil { + return err + } + } + + return nil +} + +func (e *Encoder) printf(msg string, args ...interface{}) error { + _, err := fmt.Fprintf(e.w, msg, args...) + return err +} diff --git a/plumbing/format/config/encoder_test.go b/plumbing/format/config/encoder_test.go new file mode 100644 index 0000000..5335b83 --- /dev/null +++ b/plumbing/format/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/plumbing/format/config/fixtures_test.go b/plumbing/format/config/fixtures_test.go new file mode 100644 index 0000000..12ff288 --- /dev/null +++ b/plumbing/format/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/plumbing/format/config/option.go b/plumbing/format/config/option.go new file mode 100644 index 0000000..dbb401c --- /dev/null +++ b/plumbing/format/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/plumbing/format/config/option_test.go b/plumbing/format/config/option_test.go new file mode 100644 index 0000000..8588de1 --- /dev/null +++ b/plumbing/format/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/plumbing/format/config/section.go b/plumbing/format/config/section.go new file mode 100644 index 0000000..1844913 --- /dev/null +++ b/plumbing/format/config/section.go @@ -0,0 +1,87 @@ +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 *Section) 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 *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 *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 +} + +func (s *Section) HasSubsection(name string) bool { + for _, ss := range s.Subsections { + if ss.IsName(name) { + return true + } + } + + return false +} + +func (s *Subsection) IsName(name string) bool { + return s.Name == name +} + +func (s *Subsection) Option(key string) string { + return s.Options.Get(key) +} + +func (s *Subsection) AddOption(key string, value string) *Subsection { + s.Options = s.Options.withAddedOption(key, value) + 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 +} diff --git a/plumbing/format/config/section_test.go b/plumbing/format/config/section_test.go new file mode 100644 index 0000000..cfd9f3f --- /dev/null +++ b/plumbing/format/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) +} |