diff --git a/README.md b/README.md index fa1aebe..4417a4c 100644 --- a/README.md +++ b/README.md @@ -113,6 +113,30 @@ git-profile use `use` must be executed inside a Git repository. +### `unuse` + +Remove the applied profile entries from the current Git repository: + +```bash +git-profile unuse work +``` + +Under the hood, this unsets the local Git config values, for example: + +```bash +git config --local --unset user.name +git config --local --unset user.email +git config --local --unset user.signingkey +``` + +Run without arguments to remove the currently applied profile: + +```bash +git-profile unuse +``` + +`unuse` must be executed inside a Git repository. + ### `current` Show the currently selected profile for the current repository: diff --git a/cmd/interfaces.go b/cmd/interfaces.go index 24fab3d..3c68fe4 100644 --- a/cmd/interfaces.go +++ b/cmd/interfaces.go @@ -21,4 +21,5 @@ type vcs interface { IsRepository() bool Get(key string) (string, error) Set(key string, value string) error + Unset(key string) error } diff --git a/cmd/mock.go b/cmd/mock.go index e465b42..1432cd4 100644 --- a/cmd/mock.go +++ b/cmd/mock.go @@ -397,6 +397,9 @@ func (mock *storageMock) StoreCalls() []struct { // SetFunc: func(key string, value string) error { // panic("mock out the Set method") // }, +// UnsetFunc: func(key string) error { +// panic("mock out the Unset method") +// }, // } // // // use mockedvcs in code that requires vcs @@ -413,6 +416,9 @@ type vcsMock struct { // SetFunc mocks the Set method. SetFunc func(key string, value string) error + // UnsetFunc mocks the Unset method. + UnsetFunc func(key string) error + // calls tracks calls to the methods. calls struct { // Get holds details about calls to the Get method. @@ -430,10 +436,16 @@ type vcsMock struct { // Value is the value argument value. Value string } + // Unset holds details about calls to the Unset method. + Unset []struct { + // Key is the key argument value. + Key string + } } lockGet sync.RWMutex lockIsRepository sync.RWMutex lockSet sync.RWMutex + lockUnset sync.RWMutex } // Get calls GetFunc. @@ -530,3 +542,35 @@ func (mock *vcsMock) SetCalls() []struct { mock.lockSet.RUnlock() return calls } + +// Unset calls UnsetFunc. +func (mock *vcsMock) Unset(key string) error { + if mock.UnsetFunc == nil { + panic("vcsMock.UnsetFunc: method is nil but vcs.Unset was just called") + } + callInfo := struct { + Key string + }{ + Key: key, + } + mock.lockUnset.Lock() + mock.calls.Unset = append(mock.calls.Unset, callInfo) + mock.lockUnset.Unlock() + return mock.UnsetFunc(key) +} + +// UnsetCalls gets all the calls that were made to Unset. +// Check the length with: +// +// len(mockedvcs.UnsetCalls()) +func (mock *vcsMock) UnsetCalls() []struct { + Key string +} { + var calls []struct { + Key string + } + mock.lockUnset.RLock() + calls = mock.calls.Unset + mock.lockUnset.RUnlock() + return calls +} diff --git a/cmd/root.go b/cmd/root.go index be6114c..00f69a9 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -87,6 +87,7 @@ func (c *Cmd) init() { Export(c.config), Import(c.config), Use(c.config, c.git), + Unuse(c.config, c.git), Version(c), ) diff --git a/cmd/unuse.go b/cmd/unuse.go new file mode 100644 index 0000000..19c0154 --- /dev/null +++ b/cmd/unuse.go @@ -0,0 +1,77 @@ +package cmd + +import ( + "fmt" + "os" + + "github.com/spf13/cobra" + + "github.com/dotzero/git-profile/internal/ui" +) + +// Unuse returns `unuse` command +func Unuse(cfg storage, v vcs) *cobra.Command { + return unuseCommand(cfg, v) +} + +func unuseCommand(cfg storage, v vcs) *cobra.Command { + return &cobra.Command{ + Use: "unuse [profile]", + Aliases: []string{"uu"}, + Short: "Unuse a profile", + Long: "Removes the applied profile entries from the current git repository.", + Example: multiline( + `git-profile unuse`, + `git-profile unuse my-profile`, + ), + PreRun: func(cmd *cobra.Command, _ []string) { + check(cmd, cfg, v) + }, + Run: func(cmd *cobra.Command, args []string) { + profile, err := unuseProfileResolve(args, v) + if err != nil { + ui.PrintErrln(cmd, ui.ErrorStyle, "%s", err) + os.Exit(0) + } + + profileUnapply(cmd, cfg, v, profile) + }, + } +} + +func unuseProfileResolve(args []string, v vcs) (string, error) { + if len(args) > 0 { + return args[0], nil + } + + profile, err := v.Get(currentProfileKey) + if len(profile) == 0 || err != nil { + return "", fmt.Errorf("There is no profile applied to current git repository") + } + + return profile, nil +} + +func profileUnapply(cmd *cobra.Command, cfg storage, v vcs, profile string) { + entries, ok := cfg.Lookup(profile) + if !ok { + ui.PrintErrln(cmd, ui.ErrorStyle, "There is no profile with `%s` name", profile) + os.Exit(0) + } + + for _, entry := range entries { + err := v.Unset(entry.Key) + if err != nil { + ui.PrintErrln(cmd, ui.ErrorStyle, "Unable to interact with git to remove current profile: %s", err) + os.Exit(1) + } + } + + err := v.Unset(currentProfileKey) + if err != nil { + ui.PrintErrln(cmd, ui.ErrorStyle, "Unable to interact with git to remove current profile: %s", err) + os.Exit(1) + } + + ui.Println(cmd, ui.SuccessStyle, "Successfully removed `%s` profile from current git repository.", profile) +} diff --git a/cmd/unuse_test.go b/cmd/unuse_test.go new file mode 100644 index 0000000..31cc0b2 --- /dev/null +++ b/cmd/unuse_test.go @@ -0,0 +1,90 @@ +package cmd + +import ( + "bytes" + "fmt" + "testing" + + "github.com/matryer/is" + + "github.com/dotzero/git-profile/internal/config" +) + +func TestUnuse(t *testing.T) { + is := is.New(t) + + cfg := &storageMock{ + LenFunc: func() int { + return 1 + }, + LookupFunc: func(name string) ([]config.Entry, bool) { + return []config.Entry{ + {Key: "user.email", Value: "work@example.com"}, + }, true + }, + } + + var unset []string + + vcs := &vcsMock{ + IsRepositoryFunc: func() bool { + return true + }, + UnsetFunc: func(key string) error { + unset = append(unset, key) + return nil + }, + } + + var b bytes.Buffer + + cmd := Unuse(cfg, vcs) + + cmd.SetOut(&b) + cmd.SetArgs([]string{"profile"}) + err := cmd.Execute() + + is.NoErr(err) + is.Equal(trim(b.String()), "Successfully removed `profile` profile from current git repository.") + is.Equal(unset, []string{"user.email", currentProfileKey}) +} + +func TestUnuseProfileResolveArg(t *testing.T) { + is := is.New(t) + + profile, err := unuseProfileResolve([]string{"work"}, &vcsMock{}) + + is.NoErr(err) + is.Equal(profile, "work") +} + +func TestUnuseProfileResolveCurrent(t *testing.T) { + is := is.New(t) + + vcs := &vcsMock{ + GetFunc: func(key string) (string, error) { + is.Equal(key, currentProfileKey) + return "home", nil + }, + } + + profile, err := unuseProfileResolve(nil, vcs) + + is.NoErr(err) + is.Equal(profile, "home") +} + +func TestUnuseProfileResolveNoneApplied(t *testing.T) { + is := is.New(t) + + vcs := &vcsMock{ + GetFunc: func(_ string) (string, error) { + return "", fmt.Errorf("boom") + }, + } + + _, err := unuseProfileResolve(nil, vcs) + + is.True(err != nil) + is.Equal(err.Error(), "There is no profile applied to current git repository") +} diff --git a/internal/git/git.go b/internal/git/git.go index 08c606f..61ba54d 100644 --- a/internal/git/git.go +++ b/internal/git/git.go @@ -1,10 +1,15 @@ package git import ( + "errors" "os/exec" "strings" ) +// gitConfigKeyNotFound is the exit code git returns from +// `git config --unset` when the key is not present. +const gitConfigKeyNotFound = 5 + // Git is a vcs type Git struct { exec func(name string, arg ...string) *exec.Cmd @@ -37,3 +42,19 @@ func (g *Git) Set(key string, value string) error { return err } + +// Unset removes a key from git local config. +// A missing key is not treated as an error. +func (g *Git) Unset(key string) error { + _, err := g.exec("git", "config", "--local", "--unset", key).CombinedOutput() + if err != nil { + var exitErr *exec.ExitError + if errors.As(err, &exitErr) && exitErr.ExitCode() == gitConfigKeyNotFound { + return nil + } + + return err + } + + return nil +} diff --git a/internal/git/git_test.go b/internal/git/git_test.go index 8e7d20b..6b1ccf4 100644 --- a/internal/git/git_test.go +++ b/internal/git/git_test.go @@ -88,3 +88,12 @@ func TestSet(t *testing.T) { is.NoErr(err) } + +func TestUnset(t *testing.T) { + is := is.New(t) + + g := &Git{exec: mockExecCommand} + err := g.Unset("user.name") + + is.NoErr(err) +}