Files
gitea/modules/git/gitcmd/error.go
2026-09-14 14:59:18 +00:00

158 lines
4.1 KiB
Go

// Copyright 2026 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package gitcmd
import (
"context"
"errors"
"fmt"
"os/exec"
"strings"
"gitea.dev/modules/regexplru"
"gitea.dev/modules/setting"
"gitea.dev/modules/util"
)
type RunStdError interface {
error
Unwrap() error
Stderr() string
}
type runStdError struct {
err error // usually the low-level error like `*exec.ExitError`
stderr string // git command's stderr output
errMsg string // the cached error message for Error() method
}
func (r *runStdError) Error() string {
// FIXME: GIT-CMD-STDERR: it is a bad design, the stderr should not be put in the error message
// But a lot of code only checks `strings.Contains(err.Error(), "git error")`
if r.errMsg == "" {
r.errMsg = fmt.Sprintf("%s - %s", r.err.Error(), strings.TrimSpace(r.stderr))
}
return r.errMsg
}
func (r *runStdError) Unwrap() error {
return r.err
}
func (r *runStdError) Stderr() string {
return r.stderr
}
func ErrorAsStderr(err error) (string, bool) {
if runErr, ok := errors.AsType[RunStdError](err); ok {
return runErr.Stderr(), true
}
return "", false
}
func IsErrorExitCode(err error, code int) bool {
if exitError, ok := errors.AsType[*exec.ExitError](err); ok {
return exitError.ExitCode() == code
}
return false
}
func IsErrorSignalKilled(err error) bool {
var exitError *exec.ExitError
return errors.As(err, &exitError) && exitError.String() == "signal: killed"
}
func IsErrorCanceledOrKilled(err error) bool {
// When "cancel()" a git command's context, the returned error of "Run()" could be one of them:
// - context.Canceled
// - *exec.ExitError: "signal: killed"
// TODO: in the future, we need to use unified error type from gitcmd.Run to check whether it is manually canceled
return errors.Is(err, context.Canceled) || IsErrorSignalKilled(err)
}
type StderrCheck interface {
internalOnly()
}
type (
StderrPrefix string
StderrRegexp string
)
func (StderrPrefix) internalOnly() {}
func (StderrRegexp) internalOnly() {}
const (
StderrNotValidObjectName StderrPrefix = "fatal: not a valid object name"
StderrNotTreeObject StderrPrefix = "fatal: not a tree object"
StderrPathSpec StderrPrefix = "fatal: pathspec"
StderrBadRevision StderrPrefix = "fatal: bad revision"
StderrNoSuchPath StderrPrefix = "fatal: no such path"
StderrNoSuchRemote1 StderrPrefix = "fatal: no such remote" // git < 2.30, exit status 128
StderrNoSuchRemote2 StderrPrefix = "error: no such remote" // git >= 2.30. exit status 2
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?`
)
func matchStderrCheck(stderr string, checkIntf StderrCheck) (match bool) {
switch check := any(checkIntf).(type) {
case StderrPrefix:
checkLen := len(check)
if len(stderr) >= checkLen {
// Git is lowercasing the "fatal: Not a valid object name" error message
// ref: https://lore.kernel.org/git/pull.2052.git.1771836302101.gitgitgadget@gmail.com
match = util.AsciiEqualFold(stderr[:checkLen], string(check))
}
case StderrRegexp:
re, err := regexplru.SystemCache().GetCompiled(string(check))
if err != nil {
setting.PanicInDevOrTesting("invalid stderr regexp %s", check)
} else {
match = re.MatchString(stderr)
}
default:
setting.PanicInDevOrTesting("invalid stderr type %T", checkIntf)
}
return match
}
func IsStderr(err error, checks ...StderrCheck) bool {
stderr, ok := ErrorAsStderr(err)
if !ok {
return false
}
for _, checkIntf := range checks {
if matchStderrCheck(stderr, checkIntf) {
return true
}
}
return false
}
type pipelineError struct {
error
}
func (e pipelineError) Unwrap() error {
return e.error
}
func wrapPipelineError(err error) error {
if err == nil {
return nil
}
return pipelineError{err}
}
func UnwrapPipelineError(err error) (error, bool) { //nolint:revive // this is for error unwrapping
if pe, ok := errors.AsType[pipelineError](err); ok {
return pe.error, true
}
return nil, false
}