fix: classify git failures on stderr, restrict migration failure detail (#39010)

Signed-off-by: silverwind <me@silverwind.io>
Co-authored-by: wxiaoguang <wxiaoguang@gmail.com>
This commit is contained in:
silverwind
2026-09-15 10:59:34 +02:00
committed by GitHub
co-authored by wxiaoguang
parent 7b036e96c2
commit 812191c0f9
30 changed files with 136 additions and 137 deletions
-17
View File
@@ -124,20 +124,3 @@ func (err *ErrPushRejected) GenerateMessage() {
}
err.Message = strings.TrimSpace(messageBuilder.String())
}
// ErrMoreThanOne represents an error if pull request fails when there are more than one sources (branch, tag) with the same name
type ErrMoreThanOne struct {
StdOut string
StdErr string
Err error
}
// IsErrMoreThanOne checks if an error is a ErrMoreThanOne
func IsErrMoreThanOne(err error) bool {
_, ok := err.(*ErrMoreThanOne)
return ok
}
func (err *ErrMoreThanOne) Error() string {
return fmt.Sprintf("ErrMoreThanOne Error: %v: %s\n%s", err.Err, err.StdErr, err.StdOut)
}
+1 -1
View File
@@ -539,7 +539,7 @@ func (c *Command) WaitWithStderr() RunStdError {
// if no exec error but only stderr output, the stderr output is still saved in "c.cmdManagedStderr" and can be read later
return nil
}
return &runStdError{err: errWait, stderr: util.UnsafeBytesToString(c.cmdManagedStderr.Bytes())}
return NewRunStdError(errWait, util.UnsafeBytesToString(c.cmdManagedStderr.Bytes()))
}
func (c *Command) RunWithStderr(ctx context.Context) RunStdError {
+12 -3
View File
@@ -44,6 +44,10 @@ func (r *runStdError) Stderr() string {
return r.stderr
}
func NewRunStdError(err error, stderr string) RunStdError {
return &runStdError{err: err, stderr: util.NormalizeStringEOL(stderr)}
}
func ErrorAsStderr(err error) (string, bool) {
if runErr, ok := errors.AsType[RunStdError](err); ok {
return runErr.Stderr(), true
@@ -93,6 +97,9 @@ const (
StderrNoSuchRemote1 StderrPrefix = "fatal: no such remote" // git < 2.30, exit status 128
StderrNoSuchRemote2 StderrPrefix = "error: no such remote" // git >= 2.30. exit status 2
StderrAuthenticationFailed StderrPrefix = "fatal: Authentication failed for"
StderrCouldNotReadUsername StderrPrefix = "fatal: could not read Username"
StderrUnknownRevisionOrPath StderrRegexp = "^fatal: .*: unknown revision or path not in the working tree"
StderrNoMergeBase StderrRegexp = "^fatal: .*: no merge base"
StderrFileNoEnoughLines StderrRegexp = `^fatal: file .* has only \d+ lines?`
@@ -126,9 +133,11 @@ func IsStderr(err error, checks ...StderrCheck) bool {
return false
}
for _, checkIntf := range checks {
if matchStderrCheck(stderr, checkIntf) {
return true
for line := range strings.SplitSeq(stderr, "\n") { // git can emit multiple-line message in stderr
for _, checkIntf := range checks {
if matchStderrCheck(line, checkIntf) {
return true
}
}
}
return false
+2
View File
@@ -17,8 +17,10 @@ func TestIsStderr(t *testing.T) {
{StderrUnknownRevisionOrPath, "fatal: ambiguous argument 'origin': unknown revision or path not in the working tree...."},
{StderrNoMergeBase, "fatal: origin/main..HEAD: no merge base...."},
{StderrFileNoEnoughLines, "fatal: file foo/bar has only 1 line"},
{StderrAuthenticationFailed, "Cloning into 'repo'...\r\nremote: Invalid username or token.\r\nfatal: Authentication failed for 'https://host/repo.git/'\r\n"},
}
for _, tc := range cases {
assert.True(t, IsStderr(&runStdError{stderr: tc.stderr}, tc.check), "stderr: %s", tc.stderr)
}
assert.False(t, IsStderr(&runStdError{stderr: "remote: fatal: Authentication failed for 'https://host/repo.git/'\n"}, StderrAuthenticationFailed))
}
+1 -1
View File
@@ -66,7 +66,7 @@ func (err *ErrInvalidCloneAddr) Unwrap() error {
// IsRemoteNotExistError checks the prefix of the error message to see whether a remote does not exist.
func IsRemoteNotExistError(err error) bool {
return gitcmd.IsStderr(err, gitcmd.StderrNoSuchRemote1) || gitcmd.IsStderr(err, gitcmd.StderrNoSuchRemote2)
return gitcmd.IsStderr(err, gitcmd.StderrNoSuchRemote1, gitcmd.StderrNoSuchRemote2)
}
// ParseRemoteAddr checks if given remote address is valid,
+3 -3
View File
@@ -47,11 +47,11 @@ func ManagedRemoteRemove(ctx context.Context, repo RepositoryFacade, remoteName
func ParseRemoteAddressURL(ctx context.Context, repo RepositoryFacade, remoteName string) (*giturl.GitURL, error) {
addr, err := GetRemoteAddress(ctx, repo, remoteName)
if (addr == "" && err == nil) || IsRemoteNotExistError(err) {
return nil, util.NewNotExistErrorf("remote '%s' does not exist", remoteName)
}
if err != nil {
return nil, err
}
if addr == "" {
return nil, util.NewNotExistErrorf("remote '%s' does not exist", remoteName)
}
return giturl.ParseGitURL(addr)
}
-2
View File
@@ -279,8 +279,6 @@ func Push(ctx context.Context, localRepoPath string, opts PushOptions) error {
err := &ErrPushRejected{StdOut: stdout, StdErr: stderr, Err: err}
err.GenerateMessage()
return err
} else if strings.Contains(stderr, "matches more than one") {
return &ErrMoreThanOne{StdOut: stdout, StdErr: stderr, Err: err}
}
return fmt.Errorf("push failed: %w - %s\n%s", err, stderr, stdout)
}