Skip to content

Commit

Permalink
feat: support management of global and system level git config (#91)
Browse files Browse the repository at this point in the history
  • Loading branch information
purpleclay authored May 7, 2023
1 parent 21aec48 commit 0d00716
Show file tree
Hide file tree
Showing 7 changed files with 112 additions and 100 deletions.
8 changes: 8 additions & 0 deletions .deepsource.toml
Original file line number Diff line number Diff line change
Expand Up @@ -19,8 +19,16 @@
# SOFTWARE.
version = 1

test_patterns = [
"**/*_test.go"
]

[[analyzers]]
name = "go"

[analyzers.meta]
import_root = "github.com/purpleclay/gitz"

[[analyzers]]
name = "secret"
enabled = true
50 changes: 0 additions & 50 deletions .github/workflows/git-secrets.yml

This file was deleted.

5 changes: 1 addition & 4 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -14,12 +14,9 @@
# Dependency directories (remove the comment below to include it)
# vendor/

# Built dependencies
unittest-cover.txt

# Mkdocs
.cache/
site/

# VSCode
.vscode/
.vscode/
2 changes: 1 addition & 1 deletion Taskfile.yml
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ tasks:
desc: Run the unit tests
vars:
TEST_FORMAT: '{{default "" .TEST_FORMAT}}'
COVER_PROFILE: '{{default "unittest-cover.txt" .COVER_PROFILE}}'
COVER_PROFILE: '{{default "coverage.out" .COVER_PROFILE}}'
TEST_OPTIONS: '{{default "-short -race -vet=off -shuffle=on -p 1" .TEST_OPTIONS}}'
cmds:
- go test {{.TEST_OPTIONS}} -covermode=atomic -coverprofile={{.COVER_PROFILE}} {{.TEST_FORMAT}} ./...
Expand Down
100 changes: 75 additions & 25 deletions config.go
Original file line number Diff line number Diff line change
Expand Up @@ -57,54 +57,75 @@ func (e ErrInvalidConfigPath) Error() string {
return fmt.Sprintf("path: %s invalid as %s", buf.String(), e.Reason)
}

// Config attempts to query a local git config setting for its value.
// If multiple values have been set, all are returned, ordered by the
// most recent value first
func (c *Client) Config(path string) ([]string, error) {
var cmd strings.Builder
cmd.WriteString("git config --local --get-all ")
cmd.WriteString(path)

cfg, err := c.exec(cmd.String())
// Config attempts to retrieve all git config for the current repository.
// A map is returned containing each config item and its corresponding
// latest value. Values are resolved from local, system and global config
func (c *Client) Config() (map[string]string, error) {
cfg, err := c.exec("git config --list")
if err != nil {
return nil, nil
return nil, err
}

return reverse(strings.Split(cfg, "\n")...), nil
values := map[string]string{}

lines := strings.Split(cfg, "\n")
for _, line := range lines {
pos := strings.Index(line, "=")
values[line[:pos]] = line[pos+1:]
}

return values, nil
}

// ConfigL attempts to query a batch of local git config settings for
// their values. If multiple values have been set for any config item,
// all are returned, ordered by most recent value first. A partial batch
// is never returned, all config settings must exist
func (c *Client) ConfigL(paths ...string) (map[string][]string, error) {
return c.configQuery("local", paths...)
}

func (c *Client) configQuery(location string, paths ...string) (map[string][]string, error) {
if len(paths) == 0 {
return nil, nil
}

cfg := map[string][]string{}
values := map[string][]string{}

var cmd strings.Builder
for _, path := range paths {
v, err := c.Config(path)
cmd.WriteString("git config ")
cmd.WriteString("--" + location)
cmd.WriteString(" --get-all ")
cmd.WriteString(path)

cfg, err := c.exec(cmd.String())
if err != nil {
return nil, err
}
cmd.Reset()

cfg[path] = v
v := reverse(strings.Split(cfg, "\n")...)
values[path] = v
}

return cfg, nil
return values, nil
}

// ConfigSet attempts to assign a value to a local git config setting.
// If the setting already exists, a new line is added to the local git
// config, effectively assigning multiple values to the same setting
func (c *Client) ConfigSet(path, value string) error {
var cmd strings.Builder
cmd.WriteString("git config --add ")
cmd.WriteString(fmt.Sprintf("%s '%s'", path, value))
// ConfigG attempts to query a batch of global git config settings for
// their values. If multiple values have been set for any config item,
// all are returned, ordered by most recent value first. A partial batch
// is never returned, all config settings must exist
func (c *Client) ConfigG(paths ...string) (map[string][]string, error) {
return c.configQuery("global", paths...)
}

_, err := c.exec(cmd.String())
return err
// ConfigS attempts to query a batch of system git config settings for
// their values. If multiple values have been set for any config item,
// all are returned, ordered by most recent value first. A partial batch
// is never returned, all config settings must exist
func (c *Client) ConfigS(paths ...string) (map[string][]string, error) {
return c.configQuery("system", paths...)
}

// ConfigSetL attempts to batch assign values to a group of local git
Expand All @@ -113,6 +134,10 @@ func (c *Client) ConfigSet(path, value string) error {
// setting. Basic validation is performed to minimize the possibility
// of a partial batch update
func (c *Client) ConfigSetL(pairs ...string) error {
return c.configSet("local", pairs...)
}

func (c *Client) configSet(location string, pairs ...string) error {
if len(pairs) == 0 {
return nil
}
Expand All @@ -127,15 +152,40 @@ func (c *Client) ConfigSetL(pairs ...string) error {
}
}

var cmd strings.Builder
for i := 0; i < len(pairs); i += 2 {
if err := c.ConfigSet(pairs[i], pairs[i+1]); err != nil {
cmd.WriteString("git config ")
cmd.WriteString("--" + location)
cmd.WriteString(" --add ")
cmd.WriteString(fmt.Sprintf("%s '%s'", pairs[i], pairs[i+1]))

if _, err := c.exec(cmd.String()); err != nil {
return err
}
cmd.Reset()
}

return nil
}

// ConfigSetG attempts to batch assign values to a group of global git
// config settings. If any setting exists, a new line is added to the
// local git config, effectively assigning multiple values to the same
// setting. Basic validation is performed to minimize the possibility
// of a partial batch update
func (c *Client) ConfigSetG(pairs ...string) error {
return c.configSet("global", pairs...)
}

// ConfigSetS attempts to batch assign values to a group of system git
// config settings. If any setting exists, a new line is added to the
// local git config, effectively assigning multiple values to the same
// setting. Basic validation is performed to minimize the possibility
// of a partial batch update
func (c *Client) ConfigSetS(pairs ...string) error {
return c.configSet("system", pairs...)
}

// CheckConfigPath performs rudimentary checks to ensure the config path
// conforms to the git config specification. A config path is invalid if:
//
Expand Down
33 changes: 13 additions & 20 deletions config_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,6 @@ SOFTWARE.
package git_test

import (
"fmt"
"testing"

git "github.com/purpleclay/gitz"
Expand All @@ -34,26 +33,30 @@ import (

func TestConfig(t *testing.T) {
gittest.InitRepository(t)
setConfig(t, "user.name", "joker")
gittest.ConfigSet(t, "user.name", "joker", "user.email", "[email protected]")

client, _ := git.NewClient()
cfg, err := client.Config("user.name")
cfg, err := client.Config()

require.NoError(t, err)
require.Len(t, cfg, 2)
assert.Equal(t, "joker", cfg[0])
assert.Equal(t, gittest.DefaultAuthorName, cfg[1])
assert.Equal(t, "joker", cfg["user.name"])
assert.Equal(t, "[email protected]", cfg["user.email"])
}

func setConfig(t *testing.T, path, value string) {
t.Helper()
_, err := gittest.Exec(t, fmt.Sprintf("git config --add %s '%s'", path, value))
func TestConfigOnlyLatestValues(t *testing.T) {
gittest.InitRepository(t)
gittest.ConfigSet(t, "user.name", "joker", "user.name", "scarecrow")

client, _ := git.NewClient()
cfg, err := client.Config()

require.NoError(t, err)
assert.Equal(t, "scarecrow", cfg["user.name"])
}

func TestConfigL(t *testing.T) {
gittest.InitRepository(t)
setConfig(t, "user.name", "alfred")
gittest.ConfigSet(t, "user.name", "alfred")

client, _ := git.NewClient()
cfg, err := client.ConfigL("user.name", "user.email")
Expand All @@ -67,16 +70,6 @@ func TestConfigL(t *testing.T) {
assert.Equal(t, gittest.DefaultAuthorEmail, cfg["user.email"][0])
}

func TestConfigSet(t *testing.T) {
gittest.InitRepository(t)

client, _ := git.NewClient()
err := client.ConfigSet("user.age", "unknown")

require.NoError(t, err)
configEquals(t, "user.age", "unknown")
}

func configEquals(t *testing.T, path, expected string) {
t.Helper()
cfg, err := gittest.Exec(t, "git config --get "+path)
Expand Down
14 changes: 14 additions & 0 deletions gittest/repository.go
Original file line number Diff line number Diff line change
Expand Up @@ -503,6 +503,20 @@ func MustExec(t *testing.T, cmd string) string {
return out
}

// ConfigSet will set any number of local git config items for the current
// repository. Input must contain an even number of pairs. The following git
// command is executed for each config pair:
//
// git config --add <path> '<value>'
func ConfigSet(t *testing.T, pairs ...string) {
t.Helper()

require.Equal(t, len(pairs)%2, 0, "mismatch in number of config pairs")
for i := 0; i < len(pairs); i += 2 {
MustExec(t, fmt.Sprintf("git config --add %s '%s'", pairs[i], pairs[i+1]))
}
}

// Tags returns a list of all local tags associated with the current
// repository. Raw output is returned from the git command:
//
Expand Down

0 comments on commit 0d00716

Please sign in to comment.