diff options
-rw-r--r-- | _examples/config/main.go | 65 | ||||
-rw-r--r-- | config/config.go | 49 | ||||
-rw-r--r-- | config/config_test.go | 160 | ||||
-rw-r--r-- | plumbing/format/config/merged.go | 215 | ||||
-rw-r--r-- | storage/filesystem/config.go | 52 | ||||
-rw-r--r-- | storage/filesystem/dotgit/dotgit.go | 50 |
6 files changed, 559 insertions, 32 deletions
diff --git a/_examples/config/main.go b/_examples/config/main.go new file mode 100644 index 0000000..22dc447 --- /dev/null +++ b/_examples/config/main.go @@ -0,0 +1,65 @@ +package main + +import ( + "github.com/go-git/go-git/v5" + . "github.com/go-git/go-git/v5/_examples" + format "github.com/go-git/go-git/v5/plumbing/format/config" + + "github.com/go-git/go-git/v5/config" +) + +// Example of how to: +// - Access basic local (i.e. ./.git/config) configuration params +// - Set basic local config params +// - Set custom local config params +// - Access custom local config params +// - Set global config params +// - Access global & system config params + +func main() { + // Open this repository + // Info("git init") + // r, err := git.Init(memory.NewStorage(), nil) + Info("open local git repo") + r, err := git.PlainOpen(".") + CheckIfError(err) + + // Load the configuration + cfg, err := r.Config() + CheckIfError(err) + + // Get core local config params + if cfg.Core.IsBare { + Info("repo is bare") + } else { + Info("repo is not bare") + } + + Info("worktree is %s", cfg.Core.Worktree) + + // Set basic local config params + cfg.Remotes["origin"] = &config.RemoteConfig{ + Name: "origin", + URLs: []string{"git@github.com:mcuadros/go-git.git"}, + } + + Info("origin remote: %+v", cfg.Remotes["origin"]) + + // Set local custom config param + cfg.Merged.LocalConfig().AddOption("custom", format.NoSubsection, "name", "Local Name") + + // Set global config param (~/.gitconfig) + cfg.Merged.GlobalConfig().AddOption("custom", format.NoSubsection, "name", "Global Name") + + // Get custom config param (merged in the same way git does: system -> global -> local) + Info("custom.name is %s", cfg.Merged.Section("custom").Option("name")) + + // Get system config params (/etc/gitconfig) + systemSections := cfg.Merged.SystemConfig().Sections + for _, ss := range systemSections { + Info("System section: %s", ss.Name) + for _, o := range ss.Options { + Info("\tOption: %s = %s", o.Key, o.Value) + } + } +} diff --git a/config/config.go b/config/config.go index bec35b0..2b427cb 100644 --- a/config/config.go +++ b/config/config.go @@ -66,6 +66,9 @@ type Config struct { // preserve the parsed information from the original format, to avoid // dropping unsupported fields. Raw *format.Config + // Merged contains the raw form of how git views the system (/etc/gitconfig), + // global (~/.gitconfig), and local (./.git/config) config params. + Merged *format.Merged } // NewConfig returns a new empty Config. @@ -74,9 +77,11 @@ func NewConfig() *Config { Remotes: make(map[string]*RemoteConfig), Submodules: make(map[string]*Submodule), Branches: make(map[string]*Branch), - Raw: format.New(), + Merged: format.NewMerged(), } + config.Raw = config.Merged.LocalConfig() + config.Pack.Window = DefaultPackWindow return config @@ -129,25 +134,38 @@ const ( // Unmarshal parses a git-config file and stores it. func (c *Config) Unmarshal(b []byte) error { + return c.UnmarshalScoped(format.LocalScope, b) +} + +func (c *Config) UnmarshalScoped(scope format.Scope, b []byte) error { r := bytes.NewBuffer(b) d := format.NewDecoder(r) - c.Raw = format.New() - if err := d.Decode(c.Raw); err != nil { - return err - } + c.Merged.ResetScopedConfig(scope) - c.unmarshalCore() - if err := c.unmarshalPack(); err != nil { + if err := d.Decode(c.Merged.ScopedConfig(scope)); err != nil { return err } - unmarshalSubmodules(c.Raw, c.Submodules) - if err := c.unmarshalBranches(); err != nil { - return err + if scope == format.LocalScope { + c.Raw = c.Merged.LocalConfig() + + c.unmarshalCore() + if err := c.unmarshalPack(); err != nil { + return err + } + unmarshalSubmodules(c.Raw, c.Submodules) + + if err := c.unmarshalBranches(); err != nil { + return err + } + + if err := c.unmarshalRemotes(); err != nil { + return err + } } - return c.unmarshalRemotes() + return nil } func (c *Config) unmarshalCore() { @@ -218,7 +236,7 @@ func (c *Config) unmarshalBranches() error { } // Marshal returns Config encoded as a git-config file. -func (c *Config) Marshal() ([]byte, error) { +func (c *Config) MarshalScope(scope format.Scope) ([]byte, error) { c.marshalCore() c.marshalPack() c.marshalRemotes() @@ -226,13 +244,18 @@ func (c *Config) Marshal() ([]byte, error) { c.marshalBranches() buf := bytes.NewBuffer(nil) - if err := format.NewEncoder(buf).Encode(c.Raw); err != nil { + cfg := c.Merged.ScopedConfig(scope) + if err := format.NewEncoder(buf).Encode(cfg); err != nil { return nil, err } return buf.Bytes(), nil } +func (c *Config) Marshal() ([]byte, error) { + return c.MarshalScope(format.LocalScope) +} + func (c *Config) marshalCore() { s := c.Raw.Section(coreSection) s.SetOption(bareKey, fmt.Sprintf("%t", c.Core.IsBare)) diff --git a/config/config_test.go b/config/config_test.go index e5e3be5..a2ece2a 100644 --- a/config/config_test.go +++ b/config/config_test.go @@ -3,6 +3,7 @@ package config import ( . "gopkg.in/check.v1" "github.com/go-git/go-git/v5/plumbing" + format "github.com/go-git/go-git/v5/plumbing/format/config" ) type ConfigSuite struct{} @@ -60,6 +61,76 @@ func (s *ConfigSuite) TestUnmarshal(c *C) { c.Assert(cfg.Branches["master"].Merge, Equals, plumbing.ReferenceName("refs/heads/master")) } +func (s *ConfigSuite) TestMergedUnmarshal(c *C) { + localInput := []byte(`[core] + bare = true + worktree = foo + commentchar = bar +[pack] + window = 20 +[remote "origin"] + url = git@github.com:mcuadros/go-git.git + fetch = +refs/heads/*:refs/remotes/origin/* +[remote "alt"] + url = git@github.com:mcuadros/go-git.git + url = git@github.com:src-d/go-git.git + fetch = +refs/heads/*:refs/remotes/origin/* + fetch = +refs/pull/*:refs/remotes/origin/pull/* +[remote "win-local"] + url = X:\\Git\\ +[submodule "qux"] + path = qux + url = https://github.com/foo/qux.git + branch = bar +[branch "master"] + remote = origin + merge = refs/heads/master +[user] + name = Override +`) + + globalInput := []byte(` +[user] + name = Soandso + email = soandso@example.com +[core] + editor = nvim +[push] + default = simple +`) + + cfg := NewConfig() + + err := cfg.UnmarshalScoped(format.LocalScope, localInput) + c.Assert(err, IsNil) + + err = cfg.UnmarshalScoped(format.GlobalScope, globalInput) + c.Assert(err, IsNil) + + c.Assert(cfg.Core.IsBare, Equals, true) + c.Assert(cfg.Core.Worktree, Equals, "foo") + c.Assert(cfg.Core.CommentChar, Equals, "bar") + c.Assert(cfg.Pack.Window, Equals, uint(20)) + c.Assert(cfg.Remotes, HasLen, 3) + c.Assert(cfg.Remotes["origin"].Name, Equals, "origin") + c.Assert(cfg.Remotes["origin"].URLs, DeepEquals, []string{"git@github.com:mcuadros/go-git.git"}) + c.Assert(cfg.Remotes["origin"].Fetch, DeepEquals, []RefSpec{"+refs/heads/*:refs/remotes/origin/*"}) + c.Assert(cfg.Remotes["alt"].Name, Equals, "alt") + c.Assert(cfg.Remotes["alt"].URLs, DeepEquals, []string{"git@github.com:mcuadros/go-git.git", "git@github.com:src-d/go-git.git"}) + c.Assert(cfg.Remotes["alt"].Fetch, DeepEquals, []RefSpec{"+refs/heads/*:refs/remotes/origin/*", "+refs/pull/*:refs/remotes/origin/pull/*"}) + c.Assert(cfg.Remotes["win-local"].Name, Equals, "win-local") + c.Assert(cfg.Remotes["win-local"].URLs, DeepEquals, []string{"X:\\Git\\"}) + c.Assert(cfg.Submodules, HasLen, 1) + c.Assert(cfg.Submodules["qux"].Name, Equals, "qux") + c.Assert(cfg.Submodules["qux"].URL, Equals, "https://github.com/foo/qux.git") + c.Assert(cfg.Submodules["qux"].Branch, Equals, "bar") + c.Assert(cfg.Branches["master"].Remote, Equals, "origin") + c.Assert(cfg.Branches["master"].Merge, Equals, plumbing.ReferenceName("refs/heads/master")) + c.Assert(cfg.Merged.Section("user").Option("name"), Equals, "Override") + c.Assert(cfg.Merged.Section("user").Option("email"), Equals, "soandso@example.com") + c.Assert(cfg.Merged.Section("push").Option("default"), Equals, "simple") +} + func (s *ConfigSuite) TestMarshal(c *C) { output := []byte(`[core] bare = true @@ -119,6 +190,95 @@ func (s *ConfigSuite) TestMarshal(c *C) { c.Assert(string(b), Equals, string(output)) } +func (s *ConfigSuite) TestMergedMarshal(c *C) { + localOutput := []byte(`[user] + name = Override +[custom] + key = value +[core] + bare = true + worktree = bar +[pack] + window = 20 +[remote "alt"] + url = git@github.com:mcuadros/go-git.git + url = git@github.com:src-d/go-git.git + fetch = +refs/heads/*:refs/remotes/origin/* + fetch = +refs/pull/*:refs/remotes/origin/pull/* +[remote "origin"] + url = git@github.com:mcuadros/go-git.git +[remote "win-local"] + url = "X:\\Git\\" +[submodule "qux"] + url = https://github.com/foo/qux.git +[branch "master"] + remote = origin + merge = refs/heads/master +`) + + globalOutput := []byte(`[user] + name = Soandso + email = soandso@example.com +[core] + editor = nvim +[push] + default = simple +`) + + cfg := NewConfig() + + cfg.Core.IsBare = true + cfg.Core.Worktree = "bar" + cfg.Pack.Window = 20 + cfg.Remotes["origin"] = &RemoteConfig{ + Name: "origin", + URLs: []string{"git@github.com:mcuadros/go-git.git"}, + } + + cfg.Remotes["alt"] = &RemoteConfig{ + Name: "alt", + URLs: []string{"git@github.com:mcuadros/go-git.git", "git@github.com:src-d/go-git.git"}, + Fetch: []RefSpec{"+refs/heads/*:refs/remotes/origin/*", "+refs/pull/*:refs/remotes/origin/pull/*"}, + } + + cfg.Remotes["win-local"] = &RemoteConfig{ + Name: "win-local", + URLs: []string{"X:\\Git\\"}, + } + + cfg.Submodules["qux"] = &Submodule{ + Name: "qux", + URL: "https://github.com/foo/qux.git", + } + + cfg.Branches["master"] = &Branch{ + Name: "master", + Remote: "origin", + Merge: "refs/heads/master", + } + + cfg.Merged.GlobalConfig().Section("user").SetOption("name", "Soandso") + cfg.Merged.LocalConfig().Section("user").SetOption("name", "Override") + cfg.Merged.GlobalConfig().Section("user").SetOption("email", "soandso@example.com") + cfg.Merged.GlobalConfig().Section("core").AddOption("editor", "nvim") + cfg.Merged.LocalConfig().Section("custom").SetOption("key", "value") + cfg.Merged.GlobalConfig().Section("push").AddOption("default", "simple") + + c.Assert(cfg.Merged.Section("user").Option("name"), Equals, "Override") + + localBytes, err := cfg.Marshal() + c.Assert(err, IsNil) + c.Assert(string(localBytes), Equals, string(localOutput)) + + globalBytes, err := cfg.MarshalScope(format.GlobalScope) + c.Assert(err, IsNil) + c.Assert(string(globalBytes), Equals, string(globalOutput)) + + systemBytes, err := cfg.MarshalScope(format.SystemScope) + c.Assert(err, IsNil) + c.Assert(string(systemBytes), Equals, "") +} + func (s *ConfigSuite) TestUnmarshalMarshal(c *C) { input := []byte(`[core] bare = true diff --git a/plumbing/format/config/merged.go b/plumbing/format/config/merged.go new file mode 100644 index 0000000..3afb063 --- /dev/null +++ b/plumbing/format/config/merged.go @@ -0,0 +1,215 @@ +package config + +type Scope int + +const ( + LocalScope Scope = iota + GlobalScope + SystemScope +) + +type ScopedConfigs map[Scope]*Config + +type Merged struct { + scopedConfigs ScopedConfigs +} + +type MergedSection struct { + backingSection *Section +} + +type MergedSubsection struct { + backingSubsection *Subsection +} + +type MergedSubsections []*MergedSubsection + +func NewMerged() *Merged { + cfg := &Merged{ + scopedConfigs: make(ScopedConfigs), + } + for s := LocalScope; s <= SystemScope; s++ { + cfg.scopedConfigs[s] = New() + } + + return cfg +} + +func (m *Merged) ResetScopedConfig(scope Scope) { + m.scopedConfigs[scope] = New() +} + +func (m *Merged) ScopedConfig(scope Scope) *Config { + return m.scopedConfigs[scope] +} + +func (m *Merged) LocalConfig() *Config { + return m.ScopedConfig(LocalScope) +} + +func (m *Merged) GlobalConfig() *Config { + return m.ScopedConfig(GlobalScope) +} + +func (m *Merged) SystemConfig() *Config { + return m.ScopedConfig(SystemScope) +} + +// Config.Section creates the section if it doesn't exist, which is not +// what we want in here. +func (c *Config) hasSection(name string) bool { + sections := c.Sections + var found bool + + for _, s := range sections { + if s.IsName(name) { + found = true + break + } + } + + return found +} + +func (m *Merged) Section(name string) *MergedSection { + var mergedSection *MergedSection + + for s := SystemScope; s >= LocalScope; s-- { + if m.scopedConfigs[s].hasSection(name) { + sec := m.scopedConfigs[s].Section(name) + if mergedSection == nil { + mergedSection = NewMergedSection(sec) + } + + if mergedSection.Options() == nil { + mergedSection.backingSection.Options = sec.Options + } else { + for _, o := range sec.Options { + mergedSection.backingSection.SetOption(o.Key, o.Value) + } + } + + if mergedSection.Subsections() == nil { + mergedSection.backingSection.Subsections = sec.Subsections + } else { + for _, ss := range sec.Subsections { + if mergedSection.HasSubsection(ss.Name) { + for _, o := range ss.Options { + mergedSection.backingSection.Subsection(ss.Name).SetOption(o.Key, o.Value) + } + } else { + mergedSection.backingSection.Subsections = append(mergedSection.backingSection.Subsections, ss) + } + } + } + } + } + + if mergedSection != nil { + mergedSection.backingSection.Name = name + } + + return mergedSection +} + +func (m *Merged) AddOption(scope Scope, section string, subsection string, key string, value string) *Config { + return m.ScopedConfig(scope).AddOption(section, subsection, key, value) +} + +func (m *Merged) SetOption(scope Scope, section string, subsection string, key string, value string) *Config { + return m.ScopedConfig(scope).SetOption(section, subsection, key, value) +} + +func (m *Merged) RemoveSection(scope Scope, name string) *Config { + return m.ScopedConfig(scope).RemoveSection(name) +} + +func (m *Merged) RemoveSubsection(scope Scope, section string, subsection string) *Config { + return m.ScopedConfig(scope).RemoveSubsection(section, subsection) +} + +func copyOptions(os Options) Options { + copiedOptions := make(Options, 0) + + for _, o := range os { + copiedOptions = append(copiedOptions, o) + } + + return copiedOptions +} + +func copySubsections(ss Subsections) Subsections { + copiedSubsections := make(Subsections, 0) + + for _, ss := range ss { + copiedSubsections = append(copiedSubsections, &Subsection{ + Name: ss.Name, + Options: copyOptions(ss.Options), + }) + } + + return copiedSubsections +} + +func NewMergedSection(backing *Section) *MergedSection { + return &MergedSection{ + backingSection: &Section{ + Name: backing.Name, + Options: copyOptions(backing.Options), + Subsections: copySubsections(backing.Subsections), + }, + } +} + +func (ms *MergedSection) Name() string { + return ms.backingSection.Name +} + +func (ms *MergedSection) IsName(name string) bool { + return ms.backingSection.IsName(name) +} + +func (ms *MergedSection) Options() []*Option { + return ms.backingSection.Options +} + +func (ms *MergedSection) Option(key string) string { + return ms.backingSection.Option(key) +} + +func (ms *MergedSection) Subsections() MergedSubsections { + mss := make(MergedSubsections, 0) + for _, ss := range ms.backingSection.Subsections { + mss = append(mss, NewMergedSubsection(ss)) + } + return mss +} + +func (ms *MergedSection) Subsection(name string) *MergedSubsection { + return NewMergedSubsection(ms.backingSection.Subsection(name)) +} + +func (ms *MergedSection) HasSubsection(name string) bool { + return ms.backingSection.HasSubsection(name) +} + +func NewMergedSubsection(backing *Subsection) *MergedSubsection { + return &MergedSubsection{backingSubsection: backing} +} + +func (mss *MergedSubsection) Name() string { + return mss.backingSubsection.Name +} + +func (mss *MergedSubsection) IsName(name string) bool { + return mss.backingSubsection.IsName(name) +} + +func (mss *MergedSubsection) Options() []*Option { + return mss.backingSubsection.Options +} + +func (mss *MergedSubsection) Option(key string) string { + return mss.backingSubsection.Option(key) +} + diff --git a/storage/filesystem/config.go b/storage/filesystem/config.go index 01b35b4..dbdce54 100644 --- a/storage/filesystem/config.go +++ b/storage/filesystem/config.go @@ -5,6 +5,7 @@ import ( "os" "github.com/go-git/go-git/v5/config" + format "github.com/go-git/go-git/v5/plumbing/format/config" "github.com/go-git/go-git/v5/storage/filesystem/dotgit" "github.com/go-git/go-git/v5/utils/ioutil" ) @@ -13,10 +14,11 @@ type ConfigStorage struct { dir *dotgit.DotGit } -func (c *ConfigStorage) Config() (conf *config.Config, err error) { +func (c *ConfigStorage) Config() (*config.Config, error) { cfg := config.NewConfig() - f, err := c.dir.Config() + // local config (./.git/config) + f, err := c.dir.LocalConfig() if err != nil { if os.IsNotExist(err) { return cfg, nil @@ -29,13 +31,55 @@ func (c *ConfigStorage) Config() (conf *config.Config, err error) { b, err := stdioutil.ReadAll(f) if err != nil { + return cfg, err + } + + if err = cfg.UnmarshalScoped(format.LocalScope, b); err != nil { + return cfg, err + } + + // global config (~/.gitconfig) + f, err = c.dir.GlobalConfig() + if err != nil { + if os.IsNotExist(err) { + return cfg, nil + } + return nil, err } - if err = cfg.Unmarshal(b); err != nil { + defer ioutil.CheckClose(f, &err) + + b, err = stdioutil.ReadAll(f) + if err != nil { + return cfg, err + } + + if err = cfg.UnmarshalScoped(format.GlobalScope, b); err != nil { + return cfg, err + } + + // system config (/etc/gitconfig) + f, err = c.dir.SystemConfig() + if err != nil { + if os.IsNotExist(err) { + return cfg, nil + } + return nil, err } + defer ioutil.CheckClose(f, &err) + + b, err = stdioutil.ReadAll(f) + if err != nil { + return cfg, err + } + + if err = cfg.UnmarshalScoped(format.SystemScope, b); err != nil { + return cfg, err + } + return cfg, err } @@ -44,7 +88,7 @@ func (c *ConfigStorage) SetConfig(cfg *config.Config) (err error) { return err } - f, err := c.dir.ConfigWriter() + f, err := c.dir.LocalConfigWriter() if err != nil { return err } diff --git a/storage/filesystem/dotgit/dotgit.go b/storage/filesystem/dotgit/dotgit.go index 3ce9dae..503ce18 100644 --- a/storage/filesystem/dotgit/dotgit.go +++ b/storage/filesystem/dotgit/dotgit.go @@ -21,15 +21,17 @@ import ( ) const ( - suffix = ".git" - packedRefsPath = "packed-refs" - configPath = "config" - indexPath = "index" - shallowPath = "shallow" - modulePath = "modules" - objectsPath = "objects" - packPath = "pack" - refsPath = "refs" + suffix = ".git" + packedRefsPath = "packed-refs" + localConfigPath = "config" + globalConfigPath = ".gitconfig" + systemConfigPath = "/etc/gitconfig" + indexPath = "index" + shallowPath = "shallow" + modulePath = "modules" + objectsPath = "objects" + packPath = "pack" + refsPath = "refs" tmpPackedRefsPrefix = "._packed-refs" @@ -152,14 +154,32 @@ func (d *DotGit) Close() error { return nil } -// ConfigWriter returns a file pointer for write to the config file -func (d *DotGit) ConfigWriter() (billy.File, error) { - return d.fs.Create(configPath) +// LocalConfigWriter returns a file pointer for write to the local config file +func (d *DotGit) LocalConfigWriter() (billy.File, error) { + return d.fs.Create(localConfigPath) } -// Config returns a file pointer for read to the config file -func (d *DotGit) Config() (billy.File, error) { - return d.fs.Open(configPath) +// LocalConfig returns a file pointer for read to the local config file +func (d *DotGit) LocalConfig() (billy.File, error) { + return d.fs.Open(localConfigPath) +} + +// GlobalConfigWriter returns a file pointer for write to the global config file +func (d *DotGit) GlobalConfigWriter() (billy.File, error) { + return osfs.New(os.Getenv("HOME")).Create(globalConfigPath) +} + +// GlobalConfig returns a file pointer for read to the global config file +func (d *DotGit) GlobalConfig() (billy.File, error) { + return osfs.New(os.Getenv("HOME")).Open(globalConfigPath) +} + +// SystemConfigWriter doesn't exist because we typically do not have permission +// to write to files in /etc. + +// SystemConfig returns a file pointer for read to the system config file +func (d *DotGit) SystemConfig() (billy.File, error) { + return osfs.New("/").Open(systemConfigPath) } // IndexWriter returns a file pointer for write to the index file |