8000 Feature: user config attrs by cap10morgan · Pull Request #23 · go-git/go-git · GitHub
[go: up one dir, main page]

Skip to content

Feature: user config attrs #23

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8000
197 changes: 196 additions & 1 deletion config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,57 @@ var (
ErrRemoteConfigEmptyName = errors.New("remote config: empty name")
)

// ScopedString stores strings indexed by scope for two purposes:
// 1. So we know which config file to write it back out to if requested.
// 2. So we know which value takes precedence when reading.
// Use the Value method to read its effective value (i.e. the value git would use).
// Use the Set method to set scoped values.
type ScopedString []string

func NewScopedString() ScopedString {
return make(ScopedString, format.NumScopes)
}

func (ss ScopedString) Value() string {
for s := format.LocalScope; s >= format.SystemScope; s-- {
if ss[s] != "" {
return ss[s]
}
}

return ""
}

func (ss ScopedString) Set(scope format.Scope, s string) {
ss[scope] = s
}

// ScopedBool stores bools indexed by scope for two 8000 purposes:
// 1. So we know which config file to write it back out to if requested.
// 2. So we know which value takes precedence when reading.
// Use the Value method to read its effective value (i.e. the value git would use).
// Use the Set method to set scoped values.
type ScopedBool []*bool

func NewScopedBool() ScopedBool {
return make(ScopedBool, format.NumScopes)
}

func (sb ScopedBool) Value() bool {
for s := format.LocalScope; s >= format.SystemScope; s-- {
if sb[s] != nil {
return *sb[s]
}
}

return false
}

func (sb ScopedBool) Set(scope format.Scope, b bool) {
bPrime := b // make a copy so this doesn't change out from under us
sb[scope] = &bPrime
}

// Config contains the repository configuration
// https://www.kernel.org/pub/software/scm/git/docs/git-config.html#FILES
type Config struct {
Expand All @@ -53,6 +104,29 @@ type Config struct {
Window uint
}

User struct {
Name ScopedString
Copy link
Member

Choose a reason for hiding this comment

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

Why we use ScopedStrings, instead of just load the config using the priority? Global, User, Project?

Copy link
Contributor Author
@cap10morgan cap10morgan Apr 23, 2020

Choose a reason for hiding this comment

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

The primary reason is that we need to remember the scope for when / if we write the config back out to the underlying files. We need to know where each value came from (even if someone changed it).

I think we're either going to have to overhaul the Config stuff to not allow editing scoped values in the high-level structs or keep track of this scope information somewhere else.

Copy link
Member

Choose a reason for hiding this comment

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

I see but I rather not exposing this complex API. If we need to write back the config, we should request a clean copy of the Repository one (without any merge), and save this. It will be very complex and error-prone, allow persist a config from a different context.

Copy link
Contributor Author
@cap10morgan cap10morgan Apr 23, 2020

Choose a reason for hiding this comment

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

The API on the scoped attrs is .Value() and .Set(), which doesn't seem so complex. Would it help if this were consistent across all top-level config attrs? That would be a breaking change but if we made them private struct members we could then expose their names as getter methods (and Set[Attr] for setters) so the migration would be fairly simple for users (i.e. just adding () after the attr names everywhere).

Currently there's nothing preventing writing configs because it's all based around public struct members. With un-scoped attrs, you set arbitrary struct members and then call SetConfig and suddenly your local .git/config file contains the merged result of your system, global, and local configs. I think we would need some kind of breaking change to prevent that or this library is going to mess up peoples' configs in a way that will be a ticking time bomb for them (i.e. everything will seem normal until their repo hangs onto some global or system config param they change and assume applies everywhere).

So it sounds like you're proposing some kind of new top-level, merged, read-only config. And if you want to be able to make change 10000 s to it, you have to request a specific scope's config which will be written back to that scope's corresponding file. This will be a breaking change, is that OK? Am I understanding your proposal correctly?

Copy link
Member

Choose a reason for hiding this comment

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

Here is some things that are important: Repository.Config MUST return the repository config. Since it's a method from the repository and the goal is to operate with it. Storer.SetConfig MUST not write config.Config structs with merged information.

About how to identify the config, ideally is have a MergedConfig or something but since we can't change the Config to an interface, it should be done for v6. So meanwhile we can add config.Config.Context for example.

Regarding reading the merged context, we can add config.ReadConfig(context) config.Config, as a helper for the people that wants access to this configuration. We must remember that the config merge is a very porcelain feature, so it's good to have it optional and explicit.

Internally Repository.Commit my use the new config.ReadConfig to read the config if a new flag called something like CommitOpts.UseGlobalConfig is true (by default false)

In this way, we have something that doesn't break the API or introduce unexpected behaviors. And we introduce an important feature requested by many users.

Thanks for your efforts and sorry for not being clear, but this library is a big codebase, and is hard to have it on top of the mind.

Copy link
Member

Choose a reason for hiding this comment

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

@cap10morgan are you planning to work on this soon? if not, I will take the task, I want to release a new version and I want to include this.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Sorry, I probably won't get to it soon. I'm no longer working on the project that needed this, so harder to prioritize spending work hours on it. Let me know if I can help with the effort in any other ways, though.

Email ScopedString
// UseConfigOnly instructs git to avoid trying to guess defaults for
// user.email and user.name. Note that go-git currently does not do
// anything with this value.
UseConfigOnly ScopedBool
// SigningKey is for overriding the default GPG key selection used for
// signing tags and/or commits. Note that go-git currently does not do
// anything with this value.
SigningKey ScopedString
}

Author struct {
Name ScopedString
Email ScopedString
}

Committer struct {
Name ScopedString
Email ScopedString
}

// Remotes list of repository remotes, the key of the map is the name
// of the remote, should equal to RemoteConfig.Name.
Remotes map[string]*RemoteConfig
Expand Down Expand Up @@ -84,6 +158,17 @@ func NewConfig() *Config {

config.Pack.Window = DefaultPackWindow

config.User.Name = NewScopedString()
config.User.Email = NewScopedString()
config.User.UseConfigOnly = NewScopedBool()
config.User.SigningKey = NewScopedString()

config.Author.Name = NewScopedString()
config.Author.Email = NewScopedString()

config.Committer.Name = NewScopedString()
config.Committer.Email = NewScopedString()

return config
}

Expand Down Expand Up @@ -118,6 +203,9 @@ const (
branchSection = "branch"
coreSection = "core"
packSection = "pack"
userSection = "user"
authorSection = "author"
committerSection = "committer"
fetchKey = "fetch"
urlKey = "url"
bareKey = "bare"
Expand All @@ -126,6 +214,10 @@ const (
windowKey = "window"
mergeKey = "merge"
rebaseKey = "rebase"
nameKey = "name"
emailKey = "email"
useConfigOnlyKey = "useConfigOnly"
signingKeyKey = "signingKey"

// DefaultPackWindow holds the number of previous objects used to
// generate deltas. The value 10 is the same used by git command.
Expand All @@ -143,10 +235,14 @@ func (c *Config) UnmarshalScoped(scope format.Scope, b []byte) error {

c.Merged.ResetScopedConfig(scope)

if err := d.Decode(c.Merged.ScopedConfig(scope)); err != nil {
scopedConfig := c.Merged.ScopedConfig(scope)

if err := d.Decode(scopedConfig); err != nil {
return err
}

c.unmarshalUser(scope)

if scope == format.LocalScope {
c.Raw = c.Merged.LocalConfig()

Expand All @@ -168,6 +264,59 @@ func (c *Config) UnmarshalScoped(scope format.Scope, b []byte) error {
return nil
}

func (c *Config) unmarshalUser(scope format.Scope) {
cfg := c.Merged.ScopedConfig(scope)
s := cfg.Section(userSection)

name := s.Option(nameKey)
if name != "" {
c.User.Name[scope] = name
}

email := s.Option(emailKey)
if email != "" {
c.User.Email[scope] = email
}

useConfigOnly := s.Option(useConfigOnlyKey)
if useConfigOnly != "" {
var newVal bool
if useConfigOnly == "true" {
newVal = true
}
c.User.UseConfigOnly[scope] = &newVal
}

signingKey := s.Option(signingKeyKey)
if signingKey != "" {
c.User.SigningKey[scope] = signingKey
}

s = cfg.Section(authorSection)

name = s.Option(nameKey)
if name != "" {
c.Author.Name[scope] = name
}

email = s.Option(emailKey)
if email != "" {
c.Author.Email[scope] = email
}

s = cfg.Section(committerSection)

name = s.Option(nameKey)
if name != "" {
c.Committer.Name[scope] = name
}

email = s.Option(emailKey)
if email != "" {
c.Committer.Email[scope] = email
}
}

func (c *Config) unmarshalCore() {
s := c.Raw.Section(coreSection)
if s.Options.Get(bareKey) == "true" {
Expand Down Expand Up @@ -242,6 +391,7 @@ func (c *Config) MarshalScope(scope format.Scope) ([]byte, error) {
c.marshalRemotes()
c.marshalSubmodules()
c.marshalBranches()
c.marshalUser(scope)

buf := bytes.NewBuffer(nil)
cfg := c.Merged.ScopedConfig(scope)
Expand Down Expand Up @@ -341,6 +491,51 @@ func (c *Config) marshalBranches() {
s.Subsections = newSubsections
}

func (c *Config) marshalUser(scope format.Scope) {
cfg := c.Merged.ScopedConfig(scope)
s := cfg.Section(userSection)

if n := c.User.Name[scope]; n != "" {
s.SetOption(nameKey, n)
}

if e := c.User.Email[scope]; e != "" {
s.SetOption(emailKey, e)
}

if uco := c.User.UseConfigOnly[scope]; uco != nil {
if *uco {
s.SetOption(useConfigOnlyKey, "true")
} else {
s.SetOption(useConfigOnlyKey, "false")
}
}

if sk := c.User.SigningKey[scope]; sk != "" {
s.SetOption(signingKeyKey, sk)
}

s = cfg.Section(authorSection)

if n := c.Author.Name[scope]; n != "" {
s.SetOption(nameKey, n)
}

if e := c.Author.Email[scope]; e != "" {
s.SetOption(emailKey, e)
}

s = cfg.Section(committerSection)

if n := c.Committer.Name[scope]; n != "" {
s.SetOption(nameKey, n)
}

if e := c.Committer.Email[scope]; e != "" {
s.SetOption(emailKey, e)
}
}

// RemoteConfig contains the configuration for a given remote repository.
type RemoteConfig struct {
// Name of the remote
Expand Down
32 changes: 21 additions & 11 deletions config/config_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -111,6 +111,8 @@ func (s *ConfigSuite) TestMergedUnmarshal(c *C) {
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.User.Name.Value(), Equals, "Override")
c.Assert(cfg.User.Email.Value(), Equals, "soandso@example.com")
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"})
Expand Down Expand Up @@ -191,9 +193,7 @@ func (s *ConfigSuite) TestMarshal(c *C) {
}

func (s *ConfigSuite) TestMergedMarshal(c *C) {
localOutput := []byte(`[user]
name = Override
[custom]
localOutput := []byte(`[custom]
key = value
[core]
bare = true
Expand All @@ -214,22 +214,35 @@ func (s *ConfigSuite) TestMergedMarshal(c *C) {
[branch "master"]
remote = origin
merge = refs/heads/master
[user]
name = Override
`)

globalOutput := []byte(`[user]
name = Soandso
email = soandso@example.com
[core]
globalOutput := []byte(`[core]
editor = nvim
[push]
default = simple
[user]
name = Soandso
email = soandso@example.com
useConfigOnly = true
`)

cfg := NewConfig()

cfg.Core.IsBare = true
cfg.Core.Worktree = "bar"
cfg.Pack.Window = 20

cfg.User.Name.Set(format.GlobalScope, "Soandso")
cfg.User.Email.Set(format.GlobalScope, "soandso@example.com")

cfg.User.Name.Set(format.LocalScope, "Override")

uco := true
cfg.User.UseConfigOnly.Set(format.GlobalScope, uco)
uco = false // make sure that this doesn't change the value due to pointer shenanigans

cfg.Remotes["origin"] = &RemoteConfig{
Name: "origin",
URLs: []string{"git@github.com:mcuadros/go-git.git"},
Expand Down Expand Up @@ -257,17 +270,14 @@ func (s *ConfigSuite) TestMergedMarshal(c *C) {
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(cfg.Merged.Section("user").Option("name"), Equals, "Override")
c.Assert(string(localBytes), Equals, string(localOutput))

globalBytes, err := cfg.MarshalScope(format.GlobalScope)
Expand Down
9 changes: 5 additions & 4 deletions plumbing/format/config/merged.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,9 +9,10 @@ package config
// resides in the /etc/gitconfig file.
type Scope int
const (
LocalScope Scope = iota
SystemScope Scope = iota
GlobalScope
SystemScope
LocalScope
NumScopes
)

type ScopedConfigs map[Scope]*Config
Expand Down Expand Up @@ -42,7 +43,7 @@ func NewMerged() *Merged {
cfg := &Merged{
scopedConfigs: make(ScopedConfigs),
}
for s := LocalScope; s <= SystemScope; s++ {
for s := SystemScope; s <= LocalScope; s++ {
cfg.scopedConfigs[s] = New()
}

Expand Down Expand Up @@ -112,7 +113,7 @@ func (c *Config) hasSection(name string) bool {
func (m *Merged) Section(name string) *MergedSection {
var mergedSection *MergedSection

for s := SystemScope; s >= LocalScope; s-- {
for s := SystemScope; s <= LocalScope; s++ {
if m.scopedConfigs[s].hasSection(name) {
sec := m.scopedConfigs[s].Section(name)
if mergedSection == nil {
Expand Down
0