From 0d007162d75d88fec015664458c680ab51416d18 Mon Sep 17 00:00:00 2001 From: Purple Clay Date: Sun, 7 May 2023 06:52:05 +0100 Subject: [PATCH] feat: support management of global and system level git config (#91) --- .deepsource.toml | 8 +++ .github/workflows/git-secrets.yml | 50 --------------- .gitignore | 5 +- Taskfile.yml | 2 +- config.go | 100 ++++++++++++++++++++++-------- config_test.go | 33 ++++------ gittest/repository.go | 14 +++++ 7 files changed, 112 insertions(+), 100 deletions(-) delete mode 100644 .github/workflows/git-secrets.yml diff --git a/.deepsource.toml b/.deepsource.toml index da51aab..6721085 100644 --- a/.deepsource.toml +++ b/.deepsource.toml @@ -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 diff --git a/.github/workflows/git-secrets.yml b/.github/workflows/git-secrets.yml deleted file mode 100644 index fd8b34b..0000000 --- a/.github/workflows/git-secrets.yml +++ /dev/null @@ -1,50 +0,0 @@ -# Copyright (c) 2023 Purple Clay -# -# Permission is hereby granted, free of charge, to any person obtaining a copy -# of this software and associated documentation files (the "Software"), to deal -# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -# in the Software without restriction, including without limitation the rights -# copies of the Software, and to permit persons to whom the Software is -# furnished to do so, subject to the following conditions: -# -# The above copyright notice and this permission notice shall be included in all -# copies or substantial portions of the Software. -# -# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -# SOFTWARE. - -name: git-secrets -on: - push: - branches: - - main - pull_request: - branches: - - main - -permissions: - contents: read - -jobs: - git-secrets: - if: ${{ github.actor != 'dependabot[bot]' }} - runs-on: ubuntu-latest - steps: - - name: Checkout - uses: actions/checkout@v3 - with: - fetch-depth: 0 - - - name: GitGuardian - uses: GitGuardian/gg-shield-action@master - env: - GITHUB_PUSH_BEFORE_SHA: ${{ github.event.before }} - GITHUB_PUSH_BASE_SHA: ${{ github.event.base }} - GITHUB_PULL_BASE_SHA: ${{ github.event.pull_request.base.sha }} - GITHUB_DEFAULT_BRANCH: ${{ github.event.repository.default_branch }} - GITGUARDIAN_API_KEY: ${{ secrets.GH_GITGUARDIAN_KEY }} diff --git a/.gitignore b/.gitignore index 2e6943e..ea13ae9 100644 --- a/.gitignore +++ b/.gitignore @@ -14,12 +14,9 @@ # Dependency directories (remove the comment below to include it) # vendor/ -# Built dependencies -unittest-cover.txt - # Mkdocs .cache/ site/ # VSCode -.vscode/ \ No newline at end of file +.vscode/ diff --git a/Taskfile.yml b/Taskfile.yml index 16e4da6..2dc4381 100644 --- a/Taskfile.yml +++ b/Taskfile.yml @@ -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}} ./... diff --git a/config.go b/config.go index 86fdfed..bacb49b 100644 --- a/config.go +++ b/config.go @@ -57,20 +57,24 @@ 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 @@ -78,33 +82,50 @@ func (c *Client) Config(path string) ([]string, error) { // 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 @@ -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 } @@ -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: // diff --git a/config_test.go b/config_test.go index 81edd3d..b5c6f7f 100644 --- a/config_test.go +++ b/config_test.go @@ -23,7 +23,6 @@ SOFTWARE. package git_test import ( - "fmt" "testing" git "github.com/purpleclay/gitz" @@ -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", "joker@dc.com") 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, "joker@dc.com", 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") @@ -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) diff --git a/gittest/repository.go b/gittest/repository.go index 0a48503..e84b014 100644 --- a/gittest/repository.go +++ b/gittest/repository.go @@ -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 '' +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: //