[FEAT] support searching non default branches/tags when using git-grep (#3654)

resolves https://codeberg.org/forgejo/forgejo/pulls/3639#issuecomment-1806676 and https://codeberg.org/forgejo/forgejo/pulls/3513#issuecomment-1794990

Reviewed-on: https://codeberg.org/forgejo/forgejo/pulls/3654
Reviewed-by: Earl Warren <earl-warren@noreply.codeberg.org>
Co-authored-by: Shiny Nematoda <snematoda.751k2@aleeas.com>
Co-committed-by: Shiny Nematoda <snematoda.751k2@aleeas.com>
This commit is contained in:
Shiny Nematoda 2024-05-14 15:41:03 +00:00 committed by Earl Warren
parent 9b4452fd7b
commit b6ca8abcfd
9 changed files with 101 additions and 26 deletions

View file

@ -29,6 +29,7 @@ type GrepOptions struct {
MaxResultLimit int MaxResultLimit int
ContextLineNumber int ContextLineNumber int
IsFuzzy bool IsFuzzy bool
PathSpec []setting.Glob
} }
func GrepSearch(ctx context.Context, repo *Repository, search string, opts GrepOptions) ([]*GrepResult, error) { func GrepSearch(ctx context.Context, repo *Repository, search string, opts GrepOptions) ([]*GrepResult, error) {
@ -61,15 +62,20 @@ func GrepSearch(ctx context.Context, repo *Repository, search string, opts GrepO
} else { } else {
cmd.AddOptionValues("-e", strings.TrimLeft(search, "-")) cmd.AddOptionValues("-e", strings.TrimLeft(search, "-"))
} }
// pathspec // pathspec
files := make([]string, 0, len(setting.Indexer.IncludePatterns)+len(setting.Indexer.ExcludePatterns)) files := make([]string, 0,
for _, expr := range setting.Indexer.IncludePatterns { len(setting.Indexer.IncludePatterns)+
files = append(files, expr.Pattern()) len(setting.Indexer.ExcludePatterns)+
len(opts.PathSpec))
for _, expr := range append(setting.Indexer.IncludePatterns, opts.PathSpec...) {
files = append(files, ":"+expr.Pattern())
} }
for _, expr := range setting.Indexer.ExcludePatterns { for _, expr := range setting.Indexer.ExcludePatterns {
files = append(files, ":^"+expr.Pattern()) files = append(files, ":^"+expr.Pattern())
} }
cmd.AddDynamicArguments(cmp.Or(opts.RefName, "HEAD")).AddDashesAndList(files...) cmd.AddDynamicArguments(cmp.Or(opts.RefName, "HEAD")).AddDashesAndList(files...)
opts.MaxResultLimit = cmp.Or(opts.MaxResultLimit, 50) opts.MaxResultLimit = cmp.Or(opts.MaxResultLimit, 50)
stderr := bytes.Buffer{} stderr := bytes.Buffer{}
err = cmd.Run(&RunOpts{ err = cmd.Run(&RunOpts{

View file

@ -76,3 +76,33 @@ func TestGrepLongFiles(t *testing.T) {
assert.Len(t, res, 1) assert.Len(t, res, 1)
assert.Len(t, res[0].LineCodes[0], 65*1024) assert.Len(t, res[0].LineCodes[0], 65*1024)
} }
func TestGrepRefs(t *testing.T) {
tmpDir := t.TempDir()
err := InitRepository(DefaultContext, tmpDir, false, Sha1ObjectFormat.Name())
assert.NoError(t, err)
gitRepo, err := openRepositoryWithDefaultContext(tmpDir)
assert.NoError(t, err)
defer gitRepo.Close()
assert.NoError(t, os.WriteFile(path.Join(tmpDir, "README.md"), []byte{'A'}, 0o666))
assert.NoError(t, AddChanges(tmpDir, true))
err = CommitChanges(tmpDir, CommitChangesOptions{Message: "add A"})
assert.NoError(t, err)
assert.NoError(t, gitRepo.CreateTag("v1", "HEAD"))
assert.NoError(t, os.WriteFile(path.Join(tmpDir, "README.md"), []byte{'A', 'B', 'C', 'D'}, 0o666))
assert.NoError(t, AddChanges(tmpDir, true))
err = CommitChanges(tmpDir, CommitChangesOptions{Message: "add BCD"})
assert.NoError(t, err)
res, err := GrepSearch(context.Background(), gitRepo, "a", GrepOptions{RefName: "v1"})
assert.NoError(t, err)
assert.Len(t, res, 1)
assert.Equal(t, res[0].LineCodes[0], "A")
}

View file

@ -0,0 +1 @@
Code Search for non-default branches and tags when repository indexer is disabled

View file

@ -64,7 +64,11 @@ func Search(ctx *context.Context) {
ctx.Data["CodeIndexerUnavailable"] = !code_indexer.IsAvailable(ctx) ctx.Data["CodeIndexerUnavailable"] = !code_indexer.IsAvailable(ctx)
} }
} else { } 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: ctx.Repo.RefName,
})
if err != nil { if err != nil {
ctx.ServerError("GrepSearch", err) ctx.ServerError("GrepSearch", err)
return return

View file

@ -1140,6 +1140,7 @@ PostRecentBranchCheck:
ctx.Data["TreeLink"] = treeLink ctx.Data["TreeLink"] = treeLink
ctx.Data["TreeNames"] = treeNames ctx.Data["TreeNames"] = treeNames
ctx.Data["BranchLink"] = branchLink ctx.Data["BranchLink"] = branchLink
ctx.Data["CodeIndexerDisabled"] = !setting.Indexer.RepoIndexerEnabled
ctx.HTML(http.StatusOK, tplRepoHome) ctx.HTML(http.StatusOK, tplRepoHome)
} }

View file

@ -1570,11 +1570,17 @@ func registerRoutes(m *web.Route) {
m.Group("/{username}/{reponame}", func() { m.Group("/{username}/{reponame}", func() {
if !setting.Repository.DisableStars { if !setting.Repository.DisableStars {
m.Get("/stars", repo.Stars) m.Get("/stars", context.RepoRef(), repo.Stars)
} }
m.Get("/watchers", repo.Watchers) m.Get("/watchers", context.RepoRef(), repo.Watchers)
m.Get("/search", reqRepoCodeReader, repo.Search) m.Group("/search", func() {
}, ignSignIn, context.RepoAssignment, context.RepoRef(), context.UnitTypes()) m.Get("", context.RepoRef(), repo.Search)
if !setting.Indexer.RepoIndexerEnabled {
m.Get("/branch/*", context.RepoRefByType(context.RepoRefBranch), repo.Search)
m.Get("/tag/*", context.RepoRefByType(context.RepoRefTag), repo.Search)
}
}, reqRepoCodeReader)
}, ignSignIn, context.RepoAssignment, context.UnitTypes())
m.Group("/{username}", func() { m.Group("/{username}", func() {
m.Group("/{reponame}", func() { m.Group("/{reponame}", func() {

View file

@ -8,10 +8,10 @@
<div class="repo-description"> <div class="repo-description">
<div id="repo-desc" class="gt-word-break tw-text-16"> <div id="repo-desc" class="gt-word-break tw-text-16">
{{$description := .Repository.DescriptionHTML $.Context}} {{$description := .Repository.DescriptionHTML $.Context}}
{{if $description}}<span class="description">{{$description | RenderCodeBlock}}</span>{{else if .IsRepositoryAdmin}}<span class="no-description text-italic">{{ctx.Locale.Tr "repo.no_desc"}}</span>{{end}} {{if $description}}<span class="description">{{$description | RenderCodeBlock}}</span>{{else}}<span class="no-description text-italic">{{ctx.Locale.Tr "repo.no_desc"}}</span>{{end}}
<a class="link" href="{{.Repository.Website}}">{{.Repository.Website}}</a> {{if .Repository.Website}}<a class="link" href="{{.Repository.Website}}">{{.Repository.Website}}</a>{{end}}
</div> </div>
<form class="ignore-dirty" action="{{.RepoLink}}/search" method="get"> <form class="ignore-dirty" action="{{.RepoLink}}/search/{{if .CodeIndexerDisabled}}{{.BranchNameSubURL}}{{end}}" method="get" data-test-tag="codesearch">
<div class="ui small action input"> <div class="ui small action input">
<input name="q" value="{{.Keyword}}" placeholder="{{ctx.Locale.Tr "search.code_kind"}}"> <input name="q" value="{{.Keyword}}" placeholder="{{ctx.Locale.Tr "search.code_kind"}}">
{{template "shared/search/button"}} {{template "shared/search/button"}}
@ -106,16 +106,7 @@
{{ctx.Locale.Tr "repo.use_template"}} {{ctx.Locale.Tr "repo.use_template"}}
</a> </a>
{{end}} {{end}}
{{if $isHomepage}} {{if (not $isHomepage)}}
{{/* only show the "code search" on the repo home page, it only does global search,
so do not show it when viewing file or directory to avoid misleading users (it doesn't search in a directory) */}}
<form class="ignore-dirty" action="{{.RepoLink}}/search" method="get">
<div class="ui small action input">
<input name="q" placeholder="{{ctx.Locale.Tr "search.code_kind"}}">
{{template "shared/search/button"}}
</div>
</form>
{{else}}
<span class="breadcrumb repo-path tw-ml-1"> <span class="breadcrumb repo-path tw-ml-1">
<a class="section" href="{{.RepoLink}}/src/{{.BranchNameSubURL}}" title="{{.Repository.Name}}">{{StringUtils.EllipsisString .Repository.Name 30}}</a> <a class="section" href="{{.RepoLink}}/src/{{.BranchNameSubURL}}" title="{{.Repository.Name}}">{{StringUtils.EllipsisString .Repository.Name 30}}</a>
{{- range $i, $v := .TreeNames -}} {{- range $i, $v := .TreeNames -}}

View file

@ -12,6 +12,7 @@ import (
code_indexer "code.gitea.io/gitea/modules/indexer/code" code_indexer "code.gitea.io/gitea/modules/indexer/code"
"code.gitea.io/gitea/modules/setting" "code.gitea.io/gitea/modules/setting"
"code.gitea.io/gitea/modules/test" "code.gitea.io/gitea/modules/test"
"code.gitea.io/gitea/routers"
"code.gitea.io/gitea/tests" "code.gitea.io/gitea/tests"
"github.com/PuerkitoBio/goquery" "github.com/PuerkitoBio/goquery"
@ -38,6 +39,7 @@ func TestSearchRepoNoIndexer(t *testing.T) {
func testSearchRepo(t *testing.T, indexer bool) { func testSearchRepo(t *testing.T, indexer bool) {
defer tests.PrepareTestEnv(t)() defer tests.PrepareTestEnv(t)()
defer test.MockVariableValue(&setting.Indexer.RepoIndexerEnabled, indexer)() defer test.MockVariableValue(&setting.Indexer.RepoIndexerEnabled, indexer)()
defer test.MockVariableValue(&testWebRoutes, routers.NormalRoutes())()
repo, err := repo_model.GetRepositoryByOwnerAndName(db.DefaultContext, "user2", "repo1") repo, err := repo_model.GetRepositoryByOwnerAndName(db.DefaultContext, "user2", "repo1")
assert.NoError(t, err) assert.NoError(t, err)
@ -48,6 +50,13 @@ func testSearchRepo(t *testing.T, indexer bool) {
testSearch(t, "/user2/repo1/search?q=Description&page=1", []string{"README.md"}, indexer) testSearch(t, "/user2/repo1/search?q=Description&page=1", []string{"README.md"}, indexer)
req := NewRequest(t, "HEAD", "/user2/repo1/search/branch/"+repo.DefaultBranch)
if indexer {
MakeRequest(t, req, http.StatusNotFound)
} else {
MakeRequest(t, req, http.StatusOK)
}
defer test.MockVariableValue(&setting.Indexer.IncludePatterns, setting.IndexerGlobFromString("**.txt"))() defer test.MockVariableValue(&setting.Indexer.IncludePatterns, setting.IndexerGlobFromString("**.txt"))()
defer test.MockVariableValue(&setting.Indexer.ExcludePatterns, setting.IndexerGlobFromString("**/y/**"))() defer test.MockVariableValue(&setting.Indexer.ExcludePatterns, setting.IndexerGlobFromString("**/y/**"))()

View file

@ -190,10 +190,7 @@ func TestViewRepoWithSymlinks(t *testing.T) {
// TestViewAsRepoAdmin tests PR #2167 // TestViewAsRepoAdmin tests PR #2167
func TestViewAsRepoAdmin(t *testing.T) { func TestViewAsRepoAdmin(t *testing.T) {
for user, expectedNoDescription := range map[string]bool{ for _, user := range []string{"user2", "user4"} {
"user2": true,
"user4": false,
} {
defer tests.PrepareTestEnv(t)() defer tests.PrepareTestEnv(t)()
session := loginUser(t, user) session := loginUser(t, user)
@ -206,7 +203,7 @@ func TestViewAsRepoAdmin(t *testing.T) {
repoTopics := htmlDoc.doc.Find("#repo-topics").Children() repoTopics := htmlDoc.doc.Find("#repo-topics").Children()
repoSummary := htmlDoc.doc.Find(".repository-summary").Children() repoSummary := htmlDoc.doc.Find(".repository-summary").Children()
assert.Equal(t, expectedNoDescription, noDescription.HasClass("no-description")) assert.True(t, noDescription.HasClass("no-description"))
assert.True(t, repoTopics.HasClass("repo-topic")) assert.True(t, repoTopics.HasClass("repo-topic"))
assert.True(t, repoSummary.HasClass("repository-menu")) assert.True(t, repoSummary.HasClass("repository-menu"))
} }
@ -995,3 +992,33 @@ func TestViewRepoOpenWith(t *testing.T) {
testOpenWith([]string{"test://"}) testOpenWith([]string{"test://"})
}) })
} }
func TestRepoCodeSearchForm(t *testing.T) {
defer tests.PrepareTestEnv(t)()
testSearchForm := func(t *testing.T, indexer bool) {
defer test.MockVariableValue(&setting.Indexer.RepoIndexerEnabled, indexer)()
req := NewRequest(t, "GET", "/user2/repo1/src/branch/master")
resp := MakeRequest(t, req, http.StatusOK)
htmlDoc := NewHTMLParser(t, resp.Body)
action, exists := htmlDoc.doc.Find("form[data-test-tag=codesearch]").Attr("action")
assert.True(t, exists)
branchSubURL := "/branch/master"
if indexer {
assert.NotContains(t, action, branchSubURL)
} else {
assert.Contains(t, action, branchSubURL)
}
}
t.Run("indexer disabled", func(t *testing.T) {
testSearchForm(t, false)
})
t.Run("indexer enabled", func(t *testing.T) {
testSearchForm(t, true)
})
}