diff --git a/modules/git/gitcmd/error.go b/modules/git/gitcmd/error.go index 21c0aa45255..51f6d242787 100644 --- a/modules/git/gitcmd/error.go +++ b/modules/git/gitcmd/error.go @@ -10,6 +10,7 @@ import ( "os/exec" "strings" + "gitea.dev/modules/regexplru" "gitea.dev/modules/setting" "gitea.dev/modules/util" ) @@ -70,43 +71,66 @@ func IsErrorCanceledOrKilled(err error) bool { return errors.Is(err, context.Canceled) || IsErrorSignalKilled(err) } +type StderrCheck interface { + internalOnly() +} + type ( - StderrPrefix string - StderrWildcard string + 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 StderrWildcard = "fatal: *: unknown revision or path not in the working tree" - StderrNoMergeBase StderrWildcard = "fatal: *: no merge base" + 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 IsStderr[T StderrPrefix | StderrWildcard](err error, check T) bool { +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 } - checkLen := len(check) - if len(stderr) < checkLen { - return false + + for _, checkIntf := range checks { + if matchStderrCheck(stderr, checkIntf) { + return true + } } - switch any(check).(type) { - case StderrPrefix: - // 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 - return util.AsciiEqualFold(stderr[:checkLen], string(check)) - case StderrWildcard: - prefix, remaining, _ := strings.Cut(string(check), "*") - return strings.HasPrefix(stderr, prefix) && strings.Contains(stderr, remaining) - } - setting.PanicInDevOrTesting("invalid stderr type %T", check) return false } diff --git a/modules/git/gitcmd/error_test.go b/modules/git/gitcmd/error_test.go index 7111d216dce..62d1512413e 100644 --- a/modules/git/gitcmd/error_test.go +++ b/modules/git/gitcmd/error_test.go @@ -11,11 +11,12 @@ import ( func TestIsStderr(t *testing.T) { cases := []struct { - check StderrWildcard + check StderrCheck stderr string }{ {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"}, } for _, tc := range cases { assert.True(t, IsStderr(&runStdError{stderr: tc.stderr}, tc.check), "stderr: %s", tc.stderr) diff --git a/modules/markup/html_issue.go b/modules/markup/html_issue.go index e8fc28f6e9f..71dc18a9032 100644 --- a/modules/markup/html_issue.go +++ b/modules/markup/html_issue.go @@ -123,7 +123,7 @@ func issueIndexPatternProcessor(ctx *RenderContext, node *html.Node) { case IssueNameStyleAlphanumeric: ref = references.FindRenderizableReferenceAlphanumeric(node.Data) case IssueNameStyleRegexp: - pattern, err := regexplru.GetCompiled(ctx.RenderOptions.Metas["regexp"]) + pattern, err := regexplru.UserCache().GetCompiled(ctx.RenderOptions.Metas["regexp"]) if err != nil { return } diff --git a/modules/regexplru/regexplru.go b/modules/regexplru/regexplru.go index c4b535a749f..f1c6ecd3b78 100644 --- a/modules/regexplru/regexplru.go +++ b/modules/regexplru/regexplru.go @@ -5,40 +5,37 @@ package regexplru import ( "regexp" - - "gitea.dev/modules/log" + "sync" lru "github.com/hashicorp/golang-lru/v2" ) -var lruCache *lru.Cache[string, any] +type lruItem struct { + regexp *regexp.Regexp + err error +} -func init() { - var err error - lruCache, err = lru.New[string, any](1000) - if err != nil { - log.Fatal("failed to new LRU cache, err: %v", err) - } +type RegexpCache struct { + lruCache *lru.Cache[string, *lruItem] +} + +func NewCache(size int) *RegexpCache { + lruCache, _ := lru.New[string, *lruItem](size) + return &RegexpCache{lruCache: lruCache} } // GetCompiled works like regexp.Compile, the compiled expr or error is stored in LRU cache -func GetCompiled(expr string) (r *regexp.Regexp, err error) { - v, ok := lruCache.Get(expr) +func (regexpCache *RegexpCache) GetCompiled(expr string) (r *regexp.Regexp, err error) { + v, ok := regexpCache.lruCache.Get(expr) if !ok { r, err = regexp.Compile(expr) - if err != nil { - lruCache.Add(expr, err) - return nil, err - } - lruCache.Add(expr, r) - } else { - r, ok = v.(*regexp.Regexp) - if !ok { - if err, ok = v.(error); ok { - return nil, err - } - panic("impossible") - } + regexpCache.lruCache.Add(expr, &lruItem{regexp: r, err: err}) + return r, err } - return r, nil + return v.regexp, v.err } + +var ( + UserCache = sync.OnceValue(func() *RegexpCache { return NewCache(1000) }) + SystemCache = sync.OnceValue(func() *RegexpCache { return NewCache(1000) }) +) diff --git a/modules/regexplru/regexplru_test.go b/modules/regexplru/regexplru_test.go index 4b539c31e98..8d6fe1b5566 100644 --- a/modules/regexplru/regexplru_test.go +++ b/modules/regexplru/regexplru_test.go @@ -10,17 +10,16 @@ import ( ) func TestRegexpLru(t *testing.T) { - r, err := GetCompiled("a") + r, err := UserCache().GetCompiled("a") assert.NoError(t, err) assert.True(t, r.MatchString("a")) - r, err = GetCompiled("a") + r, err = UserCache().GetCompiled("a") assert.NoError(t, err) assert.True(t, r.MatchString("a")) + assert.Equal(t, 1, UserCache().lruCache.Len()) - assert.Equal(t, 1, lruCache.Len()) - - _, err = GetCompiled("(") + _, err = UserCache().GetCompiled("(") assert.Error(t, err) - assert.Equal(t, 2, lruCache.Len()) + assert.Equal(t, 2, UserCache().lruCache.Len()) } diff --git a/services/pull/review.go b/services/pull/review.go index e07e896034d..9972f1a148b 100644 --- a/services/pull/review.go +++ b/services/pull/review.go @@ -8,7 +8,6 @@ import ( "context" "errors" "fmt" - "strings" "gitea.dev/models/db" issues_model "gitea.dev/models/issues" @@ -25,13 +24,7 @@ import ( ) func isErrBlameNotFoundOrNotEnoughLines(err error) bool { - stdErr, ok := gitcmd.ErrorAsStderr(err) - if !ok { - return false - } - notFound := strings.HasPrefix(stdErr, "fatal: no such path") - notEnoughLines := strings.HasPrefix(stdErr, "fatal: file ") && strings.Contains(stdErr, " has only ") && strings.Contains(stdErr, " lines?") - return notFound || notEnoughLines + return gitcmd.IsStderr(err, gitcmd.StderrNoSuchPath, gitcmd.StderrFileNoEnoughLines) } // ErrDismissRequestOnClosedPR represents an error when a user tries to dismiss a review associated to a closed or merged PR.