diff --git a/features/git-mob/bugs/67-getallglobal-errors-when-soloing.feature b/features/git-mob/bugs/67-getallglobal-errors-when-soloing.feature new file mode 100644 index 0000000..407dbe6 --- /dev/null +++ b/features/git-mob/bugs/67-getallglobal-errors-when-soloing.feature @@ -0,0 +1,28 @@ +#@announce-gitmob-log +#@announce-stdout @announce-stderr +Feature: 🐛 GetAllGlobal(git-mob.co-author): nonzero exit code: 1: (when soloing) + + Background: + Given I have installed git-mob into "local_bin" within the current directory + And I look for executables in "local_bin" within the current directory + + Given a file named "~/.gitconfig" with: + """ + [user] + name = Jane Doe + email = jane@example.com + """ + And a simple git repo at "example" + And I successfully run `git solo` + + Scenario: #67 git mob print + Given I cd to "example" + And I successfully run `git mob init` + When I run `git mob print` + Then the exit status should be 0 + + Scenario: #68 git commit + Given I cd to "example" + And I successfully run `git mob init` + When I run `git commit --allow-empty -m "test message"` + Then the exit status should be 0 \ No newline at end of file diff --git a/internal/gitConfig/atoms.go b/internal/gitConfig/atoms.go index d940990..6848c65 100644 --- a/internal/gitConfig/atoms.go +++ b/internal/gitConfig/atoms.go @@ -37,18 +37,22 @@ func GetGlobal(key string) string { // GetAll gets all values for a multi-valued option key. func GetAll(key string) ([]string, error) { - o, _, err := shell.SilentRun("git", "config", "--get-all", key) - if err != nil { - return make([]string, 0), err + o, exitCode, err := shell.SilentRun("git", "config", "--get-all", key) + if ExitCode(exitCode) == SectionOrKeyIsInvalid { + return make([]string, 0), nil + } else if err != nil { + return make([]string, 0), ExitCode(exitCode).Errorf(err) } return strings.Split(o, "\n"), nil } // GetAllGlobal gets all values for a multi-valued option key. func GetAllGlobal(key string) ([]string, error) { - o, _, err := shell.SilentRun("git", "config", "--global", "--get-all", key) - if err != nil { - return make([]string, 0), err + o, exitCode, err := shell.SilentRun("git", "config", "--global", "--get-all", key) + if ExitCode(exitCode) == SectionOrKeyIsInvalid { + return make([]string, 0), nil + } else if err != nil { + return make([]string, 0), ExitCode(exitCode).Errorf(err) } return strings.Split(o, "\n"), nil } @@ -56,9 +60,9 @@ func GetAllGlobal(key string) ([]string, error) { // Set sets the option, overwriting the existing value if one exists. func Set(key string, value string) error { //const { status } = SilentRun(`git config ${key} "${value}"`); - _, _, err := shell.SilentRun("git", "config", key, value) + _, exitCode, err := shell.SilentRun("git", "config", key, value) if err != nil { - return fmt.Errorf("set(%#v, %#v): %v", key, value, err) + return ExitCode(exitCode).Errorf(fmt.Errorf("set(%#v, %#v): %v", key, value, err)) } return nil } @@ -66,23 +70,23 @@ func Set(key string, value string) error { // SetGlobal sets the global option, overwriting the existing value if one exists. func SetGlobal(key string, value string) error { //const { status } = SilentRun(`git config ${key} "${value}"`); - _, _, err := shell.SilentRun("git", "config", "--global", key, value) + _, exitCode, err := shell.SilentRun("git", "config", "--global", key, value) if err != nil { - return fmt.Errorf("option '%s' has multiple values. Cannot overwrite multiple values for option '%s' with a single value", key, key) + return ExitCode(exitCode).Errorf(fmt.Errorf("option '%s' has multiple values. Cannot overwrite multiple values for option '%s' with a single value", key, key)) } return nil } // Add adds a new line to the option without altering any existing values. func Add(key string, value string) error { - _, _, err := shell.SilentRun("git", "config", "--add", key, value) - return err + _, exitCode, err := shell.SilentRun("git", "config", "--add", key, value) + return ExitCode(exitCode).Errorf(err) } // AddGlobal adds a new line to the global option without altering any existing values. func AddGlobal(key string, value string) error { - _, _, err := shell.SilentRun("git", "config", "--global", "--add", key, value) - return err + _, exitCode, err := shell.SilentRun("git", "config", "--global", "--add", key, value) + return ExitCode(exitCode).Errorf(err) } // Has checks if the given option exists in the merged configuration. @@ -106,8 +110,8 @@ func HasGlobal(key string) bool { // RemoveSection removes the given section from the configuration. func RemoveSection(key string) error { if Has(key) { - _, _, err := shell.SilentRun("git", "config", "--remove-section", key) - return err + _, exitCode, err := shell.SilentRun("git", "config", "--remove-section", key) + return ExitCode(exitCode).Errorf(err) } return nil } @@ -115,8 +119,8 @@ func RemoveSection(key string) error { // RemoveSectionGlobal removes the given section from the global configuration. func RemoveSectionGlobal(key string) error { if HasGlobal(key) { - _, _, err := shell.SilentRun("git", "config", "--global", "--remove-section", key) - return err + _, exitCode, err := shell.SilentRun("git", "config", "--global", "--remove-section", key) + return ExitCode(exitCode).Errorf(err) } return nil } @@ -124,8 +128,8 @@ func RemoveSectionGlobal(key string) error { // Remove removes the given key from the configuration. func Remove(key string) error { if Has(key) { - _, _, err := shell.SilentRun("git", "config", "--unset", key) - return err + _, exitCode, err := shell.SilentRun("git", "config", "--unset", key) + return ExitCode(exitCode).Errorf(err) } return nil } @@ -133,8 +137,8 @@ func Remove(key string) error { // RemoveGlobal removes the given key from the configuration. func RemoveGlobal(key string) error { if HasGlobal(key) { - _, _, err := shell.SilentRun("git", "config", "--global", "--unset", key) - return err + _, exitCode, err := shell.SilentRun("git", "config", "--global", "--unset", key) + return ExitCode(exitCode).Errorf(err) } return nil } @@ -142,8 +146,8 @@ func RemoveGlobal(key string) error { // RemoveAll removes all the given keys from the configuration. func RemoveAll(key string) error { if Has(key) { - _, _, err := shell.SilentRun("git", "config", "--unset-all", key) - return err + _, exitCode, err := shell.SilentRun("git", "config", "--unset-all", key) + return ExitCode(exitCode).Errorf(err) } return nil } @@ -151,8 +155,8 @@ func RemoveAll(key string) error { // RemoveAllGlobal removes all the given keys from the configuration. func RemoveAllGlobal(key string) error { if HasGlobal(key) { - _, _, err := shell.SilentRun("git", "config", "--global", "--unset-all", key) - return err + _, exitCode, err := shell.SilentRun("git", "config", "--global", "--unset-all", key) + return ExitCode(exitCode).Errorf(err) } return nil } diff --git a/internal/gitConfig/exit_codes.go b/internal/gitConfig/exit_codes.go new file mode 100644 index 0000000..cece778 --- /dev/null +++ b/internal/gitConfig/exit_codes.go @@ -0,0 +1,61 @@ +package gitConfig + +import "fmt" + +// re: https://git-scm.com/docs/git-config#_description +// [the `git config`] command will fail with non-zero status upon error. Some exit codes are... + +// ExitCode of the `git config` command. +type ExitCode int + +// ExitCodes +const ( + UnknownExitCode ExitCode = iota - 1 + CommandSuccess // 0 + SectionOrKeyIsInvalid + SectionOrNameNotProvided + FileIsInvalid + FileCannotBeWritten + CannotUnsetOptionWhichDoesNotExist + CannotUnsetSetMultipleLinesMatched + InvalidRegexp +) + +var exitCodeNames = [...]string{ + CommandSuccess: "On success, the command returns the exit code 0.", + SectionOrKeyIsInvalid: "The section or key is invalid (ret = 1)", + SectionOrNameNotProvided: "no section or name was provided (ret = 2)", + FileIsInvalid: "the config file is invalid (ret = 3)", + FileCannotBeWritten: "the config file cannot be written (ret = 4)", + CannotUnsetOptionWhichDoesNotExist: "you try to unset an option which does not exist (ret = 5)", + CannotUnsetSetMultipleLinesMatched: "you try to unset/set an option for which multiple lines match (ret = 5)", + InvalidRegexp: "you try to use an invalid regexp (ret = 6)", +} + +func (c ExitCode) String() string { + if c.IsKnown() { + return exitCodeNames[c] + } + + if c == UnknownExitCode { + return "unknown" + } + + return "undocumented" +} + +func (c ExitCode) IsKnown() bool { + return UnknownExitCode < c && c <= InvalidRegexp +} + +// Errorf wraps an error with the known interpretation from git +func (c ExitCode) Errorf(err error) error { + if err == nil { + return nil + } + + if c.IsKnown() { + return fmt.Errorf("%s: %v", c, err) + } + return fmt.Errorf("%s: %v", UnknownExitCode, err) +} diff --git a/internal/gitConfig/exit_codes_test.go b/internal/gitConfig/exit_codes_test.go new file mode 100644 index 0000000..1fc5106 --- /dev/null +++ b/internal/gitConfig/exit_codes_test.go @@ -0,0 +1,30 @@ +package gitConfig + +import "testing" + +func TestExitCode_String(t *testing.T) { + tests := []struct { + have ExitCode + want int + }{ + { + have: CommandSuccess, + want: 0, + }, + { + have: SectionOrKeyIsInvalid, + want: 1, + }, + } + for _, tt := range tests { + t.Run(tt.have.String(), func(t *testing.T) { + if got := int(tt.have); got != tt.want { + t.Errorf("String() = %v, want %v", got, tt.want) + } + + if got := ExitCode(tt.want); got != tt.have { + t.Errorf("String() = %v, want %v", got, tt.have) + } + }) + } +} diff --git a/internal/shell/silent_run.go b/internal/shell/silent_run.go index 36f4861..81ced3c 100644 --- a/internal/shell/silent_run.go +++ b/internal/shell/silent_run.go @@ -34,10 +34,10 @@ func SilentRun(name string, arg ...string) (string, int, error) { "exit.error": exitError.Error(), "exit.stderr": string(exitError.Stderr), }).WithError(err).Error("command failed") - return "", 0, fmt.Errorf("nonzero exit code: %d: %s\nexitError.Stderr: %s\ncmd.Stderr: %s\ncmd.Stdout: %s", exitError.ExitCode(), exitError.Error(), string(exitError.Stderr), stdErr.String(), out.String()) + return "", exitError.ExitCode(), fmt.Errorf("nonzero exit code: %d: %s", exitError.ExitCode(), exitError.Error()) } - lg.WithError(err).Error("command failed") - return "", 0, fmt.Errorf("%s;%s", stdErr.String(), out.String()) + lg.WithError(err).Error("command failed without exitError") + return "", -1, fmt.Errorf("%s;%s", stdErr.String(), out.String()) } lg.WithFields(log.Fields{