Skip to content

Commit a56b44b

Browse files
authored
Merge pull request #115 from fluxcd/better-push-message
Better error messages from `git push`
2 parents 3f82d6c + f0001de commit a56b44b

File tree

2 files changed

+156
-2
lines changed

2 files changed

+156
-2
lines changed

controllers/git_error_test.go

Lines changed: 102 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,102 @@
1+
/*
2+
Copyright 2020, 2021 The Flux authors
3+
4+
Licensed under the Apache License, Version 2.0 (the "License");
5+
you may not use this file except in compliance with the License.
6+
You may obtain a copy of the License at
7+
8+
http://www.apache.org/licenses/LICENSE-2.0
9+
10+
Unless required by applicable law or agreed to in writing, software
11+
distributed under the License is distributed on an "AS IS" BASIS,
12+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
See the License for the specific language governing permissions and
14+
limitations under the License.
15+
*/
16+
17+
package controllers
18+
19+
import (
20+
"errors"
21+
"testing"
22+
)
23+
24+
func TestLibgit2ErrorTidy(t *testing.T) {
25+
// this is what GitLab sends if the deploy key doesn't have write access
26+
gitlabMessage := `remote:
27+
remote: ========================================================================
28+
remote:
29+
remote: This deploy key does not have write access to this project.
30+
remote:
31+
remote: ========================================================================
32+
remote:
33+
`
34+
expectedReformat := "remote: This deploy key does not have write access to this project."
35+
36+
err := errors.New(gitlabMessage)
37+
err = libgit2PushError(err)
38+
reformattedMessage := err.Error()
39+
if reformattedMessage != expectedReformat {
40+
t.Errorf("expected %q, got %q", expectedReformat, reformattedMessage)
41+
}
42+
}
43+
44+
func TestLibgit2Multiline(t *testing.T) {
45+
// this is a hypothetical error message, in which the useful
46+
// content spans more than one line
47+
multilineMessage := `remote:
48+
remote: ========================================================================
49+
remote:
50+
remote: This deploy key does not have write access to this project.
51+
remote: You will need to create a new deploy key.
52+
remote:
53+
remote: ========================================================================
54+
remote:
55+
`
56+
expectedReformat := "remote: This deploy key does not have write access to this project. You will need to create a new deploy key."
57+
58+
err := errors.New(multilineMessage)
59+
err = libgit2PushError(err)
60+
reformattedMessage := err.Error()
61+
if reformattedMessage != expectedReformat {
62+
t.Errorf("expected %q, got %q", expectedReformat, reformattedMessage)
63+
}
64+
}
65+
66+
func TestLibgit2ErrorUnchanged(t *testing.T) {
67+
// this is (roughly) what GitHub sends if the deploy key doesn't have write access
68+
regularMessage := `remote: ERROR: deploy key does not have permissions`
69+
expectedReformat := regularMessage
70+
err := errors.New(regularMessage)
71+
err = libgit2PushError(err)
72+
reformattedMessage := err.Error()
73+
if reformattedMessage != expectedReformat {
74+
t.Errorf("expected %q, got %q", expectedReformat, reformattedMessage)
75+
}
76+
}
77+
78+
func TestGoGitErrorReplace(t *testing.T) {
79+
// this is what go-git uses as the error message is if the remote
80+
// sends a blank first line
81+
unknownMessage := `unknown error: remote: `
82+
err := errors.New(unknownMessage)
83+
err = gogitPushError(err)
84+
reformattedMessage := err.Error()
85+
if reformattedMessage == unknownMessage {
86+
t.Errorf("expected rewritten error, got %q", reformattedMessage)
87+
}
88+
}
89+
90+
func TestGoGitErrorUnchanged(t *testing.T) {
91+
// this is (roughly) what GitHub sends if the deploy key doesn't
92+
// have write access; go-git passes this on verbatim
93+
regularMessage := `remote: ERROR: deploy key does not have write access`
94+
expectedReformat := regularMessage
95+
err := errors.New(regularMessage)
96+
err = gogitPushError(err)
97+
reformattedMessage := err.Error()
98+
// test that it's been rewritten, without checking the exact content
99+
if len(reformattedMessage) > len(expectedReformat) {
100+
t.Errorf("expected %q, got %q", expectedReformat, reformattedMessage)
101+
}
102+
}

controllers/imageupdateautomation_controller.go

Lines changed: 54 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -409,22 +409,74 @@ func push(ctx context.Context, path string, repo *gogit.Repository, branch strin
409409
}
410410

411411
func pushGoGit(ctx context.Context, repo *gogit.Repository, access repoAccess) error {
412-
return repo.PushContext(ctx, &gogit.PushOptions{
412+
err := repo.PushContext(ctx, &gogit.PushOptions{
413413
Auth: access.auth.AuthMethod,
414414
})
415+
return gogitPushError(err)
416+
}
417+
418+
func gogitPushError(err error) error {
419+
if err == nil {
420+
return nil
421+
}
422+
switch strings.TrimSpace(err.Error()) {
423+
case "unknown error: remote:":
424+
// this unhelpful error arises because go-git takes the first
425+
// line of the output on stderr, and for some git providers
426+
// (GitLab, at least) the output has a blank line at the
427+
// start. The rest of stderr is thrown away, so we can't get
428+
// the actual error; but at least we know what was being
429+
// attempted, and the likely cause.
430+
return fmt.Errorf("push rejected; check git secret has write access")
431+
default:
432+
return err
433+
}
415434
}
416435

417436
func pushLibgit2(repo *libgit2.Repository, access repoAccess, branch string) error {
418437
origin, err := repo.Remotes.Lookup(originRemote)
419438
if err != nil {
420439
return err
421440
}
422-
return origin.Push([]string{fmt.Sprintf("refs/heads/%s:refs/heads/%s", branch, branch)}, &libgit2.PushOptions{
441+
err = origin.Push([]string{fmt.Sprintf("refs/heads/%s:refs/heads/%s", branch, branch)}, &libgit2.PushOptions{
423442
RemoteCallbacks: libgit2.RemoteCallbacks{
424443
CertificateCheckCallback: access.auth.CertCallback,
425444
CredentialsCallback: access.auth.CredCallback,
426445
},
427446
})
447+
return libgit2PushError(err)
448+
}
449+
450+
func libgit2PushError(err error) error {
451+
if err == nil {
452+
return err
453+
}
454+
// libgit2 returns the whole output from stderr, and we only need
455+
// the message. GitLab likes to return a banner, so as an
456+
// heuristic, strip any lines that are just "remote:" and spaces
457+
// or fencing.
458+
msg := err.Error()
459+
lines := strings.Split(msg, "\n")
460+
if len(lines) == 1 {
461+
return err
462+
}
463+
var b strings.Builder
464+
// the following removes the prefix "remote:" from each line; to
465+
// retain a bit of fidelity to the original error, start with it.
466+
b.WriteString("remote: ")
467+
468+
var appending bool
469+
for _, line := range lines {
470+
m := strings.TrimPrefix(line, "remote:")
471+
if m = strings.Trim(m, " \t="); m != "" {
472+
if appending {
473+
b.WriteString(" ")
474+
}
475+
b.WriteString(m)
476+
appending = true
477+
}
478+
}
479+
return errors.New(b.String())
428480
}
429481

430482
// --- events, metrics

0 commit comments

Comments
 (0)