diff --git a/modules/git/grep.go b/modules/git/grep.go index e7d238e586..bf6b41a886 100644 --- a/modules/git/grep.go +++ b/modules/git/grep.go @@ -29,6 +29,7 @@ type GrepOptions struct { ContextLineNumber int IsFuzzy bool MaxLineLength int // the maximum length of a line to parse, exceeding chars will be truncated + PathspecList []string } func GrepSearch(ctx context.Context, repo *Repository, search string, opts GrepOptions) ([]*GrepResult, error) { @@ -62,6 +63,7 @@ func GrepSearch(ctx context.Context, repo *Repository, search string, opts GrepO cmd.AddOptionValues("-e", strings.TrimLeft(search, "-")) } cmd.AddDynamicArguments(util.IfZero(opts.RefName, "HEAD")) + cmd.AddDashesAndList(opts.PathspecList...) opts.MaxResultLimit = util.IfZero(opts.MaxResultLimit, 50) stderr := bytes.Buffer{} err = cmd.Run(&RunOpts{ diff --git a/modules/git/grep_test.go b/modules/git/grep_test.go index 7f4ded478f..6a99f80407 100644 --- a/modules/git/grep_test.go +++ b/modules/git/grep_test.go @@ -31,6 +31,26 @@ func TestGrepSearch(t *testing.T) { }, }, res) + res, err = GrepSearch(context.Background(), repo, "void", GrepOptions{PathspecList: []string{":(glob)java-hello/*"}}) + assert.NoError(t, err) + assert.Equal(t, []*GrepResult{ + { + Filename: "java-hello/main.java", + LineNumbers: []int{3}, + LineCodes: []string{" public static void main(String[] args)"}, + }, + }, res) + + res, err = GrepSearch(context.Background(), repo, "void", GrepOptions{PathspecList: []string{":(glob,exclude)java-hello/*"}}) + assert.NoError(t, err) + assert.Equal(t, []*GrepResult{ + { + Filename: "main.vendor.java", + LineNumbers: []int{3}, + LineCodes: []string{" public static void main(String[] args)"}, + }, + }, res) + res, err = GrepSearch(context.Background(), repo, "void", GrepOptions{MaxResultLimit: 1}) assert.NoError(t, err) assert.Equal(t, []*GrepResult{ diff --git a/modules/setting/glob.go b/modules/setting/glob.go new file mode 100644 index 0000000000..8f1d24dea4 --- /dev/null +++ b/modules/setting/glob.go @@ -0,0 +1,32 @@ +// Copyright 2024 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package setting + +import "github.com/gobwas/glob" + +type GlobMatcher struct { + compiledGlob glob.Glob + patternString string +} + +var _ glob.Glob = (*GlobMatcher)(nil) + +func (g *GlobMatcher) Match(s string) bool { + return g.compiledGlob.Match(s) +} + +func (g *GlobMatcher) PatternString() string { + return g.patternString +} + +func GlobMatcherCompile(pattern string, separators ...rune) (*GlobMatcher, error) { + g, err := glob.Compile(pattern, separators...) + if err != nil { + return nil, err + } + return &GlobMatcher{ + compiledGlob: g, + patternString: pattern, + }, nil +} diff --git a/modules/setting/indexer.go b/modules/setting/indexer.go index 6877d70e3c..18585602c3 100644 --- a/modules/setting/indexer.go +++ b/modules/setting/indexer.go @@ -10,8 +10,6 @@ import ( "time" "code.gitea.io/gitea/modules/log" - - "github.com/gobwas/glob" ) // Indexer settings @@ -30,8 +28,8 @@ var Indexer = struct { RepoConnStr string RepoIndexerName string MaxIndexerFileSize int64 - IncludePatterns []glob.Glob - ExcludePatterns []glob.Glob + IncludePatterns []*GlobMatcher + ExcludePatterns []*GlobMatcher ExcludeVendored bool }{ IssueType: "bleve", @@ -93,12 +91,12 @@ func loadIndexerFrom(rootCfg ConfigProvider) { } // IndexerGlobFromString parses a comma separated list of patterns and returns a glob.Glob slice suited for repo indexing -func IndexerGlobFromString(globstr string) []glob.Glob { - extarr := make([]glob.Glob, 0, 10) +func IndexerGlobFromString(globstr string) []*GlobMatcher { + extarr := make([]*GlobMatcher, 0, 10) for _, expr := range strings.Split(strings.ToLower(globstr), ",") { expr = strings.TrimSpace(expr) if expr != "" { - if g, err := glob.Compile(expr, '.', '/'); err != nil { + if g, err := GlobMatcherCompile(expr, '.', '/'); err != nil { log.Info("Invalid glob expression '%s' (skipped): %v", expr, err) } else { extarr = append(extarr, g) diff --git a/routers/api/v1/user/repo.go b/routers/api/v1/user/repo.go index 81f8e0f3fe..d0264d6b5a 100644 --- a/routers/api/v1/user/repo.go +++ b/routers/api/v1/user/repo.go @@ -6,10 +6,8 @@ package user import ( "net/http" - "code.gitea.io/gitea/models/perm" access_model "code.gitea.io/gitea/models/perm/access" repo_model "code.gitea.io/gitea/models/repo" - unit_model "code.gitea.io/gitea/models/unit" user_model "code.gitea.io/gitea/models/user" api "code.gitea.io/gitea/modules/structs" "code.gitea.io/gitea/routers/api/v1/utils" @@ -44,7 +42,7 @@ func listUserRepos(ctx *context.APIContext, u *user_model.User, private bool) { ctx.Error(http.StatusInternalServerError, "GetUserRepoPermission", err) return } - if ctx.IsSigned && ctx.Doer.IsAdmin || permission.UnitAccessMode(unit_model.TypeCode) >= perm.AccessModeRead { + if ctx.IsSigned && ctx.Doer.IsAdmin || permission.HasAnyUnitAccess() { apiRepos = append(apiRepos, convert.ToRepo(ctx, repos[i], permission)) } } diff --git a/routers/web/repo/search.go b/routers/web/repo/search.go index d7854b2499..920a865555 100644 --- a/routers/web/repo/search.go +++ b/routers/web/repo/search.go @@ -17,6 +17,16 @@ import ( const tplSearch base.TplName = "repo/search" +func indexSettingToGitGrepPathspecList() (list []string) { + for _, expr := range setting.Indexer.IncludePatterns { + list = append(list, ":(glob)"+expr.PatternString()) + } + for _, expr := range setting.Indexer.ExcludePatterns { + list = append(list, ":(glob,exclude)"+expr.PatternString()) + } + return list +} + // Search render repository search page func Search(ctx *context.Context) { language := ctx.FormTrim("l") @@ -65,8 +75,14 @@ func Search(ctx *context.Context) { ctx.Data["CodeIndexerUnavailable"] = !code_indexer.IsAvailable(ctx) } } else { - res, err := git.GrepSearch(ctx, ctx.Repo.GitRepo, keyword, git.GrepOptions{ContextLineNumber: 3, IsFuzzy: isFuzzy}) + res, err := git.GrepSearch(ctx, ctx.Repo.GitRepo, keyword, git.GrepOptions{ + ContextLineNumber: 1, + IsFuzzy: isFuzzy, + RefName: git.RefNameFromBranch(ctx.Repo.BranchName).String(), // BranchName should be default branch or the first existing branch + PathspecList: indexSettingToGitGrepPathspecList(), + }) if err != nil { + // TODO: if no branch exists, it reports: exit status 128, fatal: this operation must be run in a work tree. ctx.ServerError("GrepSearch", err) return } diff --git a/routers/web/repo/search_test.go b/routers/web/repo/search_test.go new file mode 100644 index 0000000000..33a1610384 --- /dev/null +++ b/routers/web/repo/search_test.go @@ -0,0 +1,19 @@ +// Copyright 2024 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package repo + +import ( + "testing" + + "code.gitea.io/gitea/modules/setting" + "code.gitea.io/gitea/modules/test" + + "github.com/stretchr/testify/assert" +) + +func TestIndexSettingToGitGrepPathspecList(t *testing.T) { + defer test.MockVariableValue(&setting.Indexer.IncludePatterns, setting.IndexerGlobFromString("a"))() + defer test.MockVariableValue(&setting.Indexer.ExcludePatterns, setting.IndexerGlobFromString("b"))() + assert.Equal(t, []string{":(glob)a", ":(glob,exclude)b"}, indexSettingToGitGrepPathspecList()) +} diff --git a/tests/integration/api_repo_test.go b/tests/integration/api_repo_test.go index f33827e58b..716da762e5 100644 --- a/tests/integration/api_repo_test.go +++ b/tests/integration/api_repo_test.go @@ -13,6 +13,7 @@ import ( "code.gitea.io/gitea/models/db" access_model "code.gitea.io/gitea/models/perm/access" repo_model "code.gitea.io/gitea/models/repo" + unit_model "code.gitea.io/gitea/models/unit" "code.gitea.io/gitea/models/unittest" user_model "code.gitea.io/gitea/models/user" "code.gitea.io/gitea/modules/setting" @@ -326,6 +327,39 @@ func TestAPIOrgRepos(t *testing.T) { } } +// See issue #28483. Tests to make sure we consider more than just code unit-enabled repositories. +func TestAPIOrgReposWithCodeUnitDisabled(t *testing.T) { + defer tests.PrepareTestEnv(t)() + repo21 := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{Name: "repo21"}) + org3 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: repo21.OwnerID}) + + // Disable code repository unit. + var units []unit_model.Type + units = append(units, unit_model.TypeCode) + + if err := repo_service.UpdateRepositoryUnits(db.DefaultContext, repo21, nil, units); err != nil { + assert.Fail(t, "should have been able to delete code repository unit; failed to %v", err) + } + assert.False(t, repo21.UnitEnabled(db.DefaultContext, unit_model.TypeCode)) + + session := loginUser(t, "user2") + token := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeReadOrganization) + + req := NewRequestf(t, "GET", "/api/v1/orgs/%s/repos", org3.Name). + AddTokenAuth(token) + + resp := MakeRequest(t, req, http.StatusOK) + var apiRepos []*api.Repository + DecodeJSON(t, resp, &apiRepos) + + var repoNames []string + for _, r := range apiRepos { + repoNames = append(repoNames, r.Name) + } + + assert.Contains(t, repoNames, repo21.Name) +} + func TestAPIGetRepoByIDUnauthorized(t *testing.T) { defer tests.PrepareTestEnv(t)() user := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 4})