diff --git a/gitutil/errors.go b/gitutil/errors.go new file mode 100644 index 00000000..e7864972 --- /dev/null +++ b/gitutil/errors.go @@ -0,0 +1,77 @@ +/* +Copyright 2020, 2021 The Flux authors + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package gitutil + +import ( + "errors" + "fmt" + "strings" +) + +// GoGitError translates an error from the go-git library, or returns +// `nil` if the argument is `nil`. +func GoGitError(err error) error { + if err == nil { + return nil + } + switch strings.TrimSpace(err.Error()) { + case "unknown error: remote:": + // this unhelpful error arises because go-git takes the first + // line of the output on stderr, and for some git providers + // (GitLab, at least) the output has a blank line at the + // start. The rest of stderr is thrown away, so we can't get + // the actual error; but at least we know what was being + // attempted, and the likely cause. + return fmt.Errorf("push rejected; check git secret has write access") + default: + return err + } +} + +// LibGit2Error translates an error from the libgit2 library, or +// returns `nil` if the argument is `nil`. +func LibGit2Error(err error) error { + if err == nil { + return err + } + // libgit2 returns the whole output from stderr, and we only need + // the message. GitLab likes to return a banner, so as an + // heuristic, strip any lines that are just "remote:" and spaces + // or fencing. + msg := err.Error() + lines := strings.Split(msg, "\n") + if len(lines) == 1 { + return err + } + var b strings.Builder + // the following removes the prefix "remote:" from each line; to + // retain a bit of fidelity to the original error, start with it. + b.WriteString("remote: ") + + var appending bool + for _, line := range lines { + m := strings.TrimPrefix(line, "remote:") + if m = strings.Trim(m, " \t="); m != "" { + if appending { + b.WriteString(" ") + } + b.WriteString(m) + appending = true + } + } + return errors.New(b.String()) +} diff --git a/gitutil/errors_test.go b/gitutil/errors_test.go new file mode 100644 index 00000000..33dc0a2c --- /dev/null +++ b/gitutil/errors_test.go @@ -0,0 +1,102 @@ +/* +Copyright 2020, 2021 The Flux authors + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package gitutil + +import ( + "errors" + "testing" +) + +func TestLibgit2ErrorTidy(t *testing.T) { + // this is what GitLab sends if the deploy key doesn't have write access + gitlabMessage := `remote: +remote: ======================================================================== +remote: +remote: This deploy key does not have write access to this project. +remote: +remote: ======================================================================== +remote: +` + expectedReformat := "remote: This deploy key does not have write access to this project." + + err := errors.New(gitlabMessage) + err = LibGit2Error(err) + reformattedMessage := err.Error() + if reformattedMessage != expectedReformat { + t.Errorf("expected %q, got %q", expectedReformat, reformattedMessage) + } +} + +func TestLibgit2Multiline(t *testing.T) { + // this is a hypothetical error message, in which the useful + // content spans more than one line + multilineMessage := `remote: +remote: ======================================================================== +remote: +remote: This deploy key does not have write access to this project. +remote: You will need to create a new deploy key. +remote: +remote: ======================================================================== +remote: +` + expectedReformat := "remote: This deploy key does not have write access to this project. You will need to create a new deploy key." + + err := errors.New(multilineMessage) + err = LibGit2Error(err) + reformattedMessage := err.Error() + if reformattedMessage != expectedReformat { + t.Errorf("expected %q, got %q", expectedReformat, reformattedMessage) + } +} + +func TestLibgit2ErrorUnchanged(t *testing.T) { + // this is (roughly) what GitHub sends if the deploy key doesn't have write access + regularMessage := `remote: ERROR: deploy key does not have permissions` + expectedReformat := regularMessage + err := errors.New(regularMessage) + err = LibGit2Error(err) + reformattedMessage := err.Error() + if reformattedMessage != expectedReformat { + t.Errorf("expected %q, got %q", expectedReformat, reformattedMessage) + } +} + +func TestGoGitErrorReplace(t *testing.T) { + // this is what go-git uses as the error message is if the remote + // sends a blank first line + unknownMessage := `unknown error: remote: ` + err := errors.New(unknownMessage) + err = GoGitError(err) + reformattedMessage := err.Error() + if reformattedMessage == unknownMessage { + t.Errorf("expected rewritten error, got %q", reformattedMessage) + } +} + +func TestGoGitErrorUnchanged(t *testing.T) { + // this is (roughly) what GitHub sends if the deploy key doesn't + // have write access; go-git passes this on verbatim + regularMessage := `remote: ERROR: deploy key does not have write access` + expectedReformat := regularMessage + err := errors.New(regularMessage) + err = GoGitError(err) + reformattedMessage := err.Error() + // test that it's been rewritten, without checking the exact content + if len(reformattedMessage) > len(expectedReformat) { + t.Errorf("expected %q, got %q", expectedReformat, reformattedMessage) + } +} diff --git a/gitutil/go.mod b/gitutil/go.mod new file mode 100644 index 00000000..829ac1fa --- /dev/null +++ b/gitutil/go.mod @@ -0,0 +1,3 @@ +module github.com/fluxcd/pkg/gitutil + +go 1.15