From ee214cb886b81acb7c9cb277809d19aae42f3c7f Mon Sep 17 00:00:00 2001 From: Shiny Nematoda Date: Sun, 22 Dec 2024 12:24:29 +0000 Subject: [PATCH] feat: filepath filter for code search (#6143) Added support for searching content in a specific directory or file. Reviewed-on: https://codeberg.org/forgejo/forgejo/pulls/6143 Reviewed-by: Gusted Reviewed-by: 0ko <0ko@noreply.codeberg.org> Co-authored-by: Shiny Nematoda Co-committed-by: Shiny Nematoda --- modules/git/grep.go | 42 +++++++++-- modules/git/grep_test.go | 14 ++++ modules/indexer/code/bleve/bleve.go | 42 ++++++++--- .../bleve/tokenizer/hierarchy/hierarchy.go | 69 +++++++++++++++++++ .../tokenizer/hierarchy/hierarchy_test.go | 59 ++++++++++++++++ .../code/elasticsearch/elasticsearch.go | 35 ++++++++-- modules/indexer/code/indexer_test.go | 24 +++++-- modules/indexer/code/internal/indexer.go | 1 + modules/indexer/code/internal/util.go | 24 +------ modules/indexer/code/search.go | 2 + routers/web/explore/code.go | 2 + routers/web/repo/search.go | 12 +++- routers/web/repo/view.go | 7 ++ routers/web/user/code.go | 2 + templates/repo/home.tmpl | 22 ++++-- templates/shared/search/code/results.tmpl | 4 +- templates/shared/search/code/search.tmpl | 18 +++++ tests/integration/repo_test.go | 19 ++++- web_src/css/repo.css | 5 ++ 19 files changed, 342 insertions(+), 61 deletions(-) create mode 100644 modules/indexer/code/bleve/tokenizer/hierarchy/hierarchy.go create mode 100644 modules/indexer/code/bleve/tokenizer/hierarchy/hierarchy_test.go diff --git a/modules/git/grep.go b/modules/git/grep.go index 1daa3e8fb9..c3d4de49bd 100644 --- a/modules/git/grep.go +++ b/modules/git/grep.go @@ -36,13 +36,15 @@ const ( RegExpGrepMode ) +var GrepSearchOptions = [3]string{"exact", "union", "regexp"} + type GrepOptions struct { RefName string MaxResultLimit int MatchesPerFile int // >= git 2.38 ContextLineNumber int Mode grepMode - PathSpec []setting.Glob + Filename string } func (opts *GrepOptions) ensureDefaults() { @@ -112,12 +114,38 @@ func GrepSearch(ctx context.Context, repo *Repository, search string, opts GrepO } // pathspec - files := make([]string, 0, - len(setting.Indexer.IncludePatterns)+ - len(setting.Indexer.ExcludePatterns)+ - len(opts.PathSpec)) - for _, expr := range append(setting.Indexer.IncludePatterns, opts.PathSpec...) { - files = append(files, ":"+expr.Pattern()) + includeLen := len(setting.Indexer.IncludePatterns) + if len(opts.Filename) > 0 { + includeLen = 1 + } + files := make([]string, 0, len(setting.Indexer.ExcludePatterns)+includeLen) + if len(opts.Filename) > 0 && len(setting.Indexer.IncludePatterns) > 0 { + // if the both a global include pattern and the per search path is defined + // we only include results where the path matches the globally set pattern + // (eg, global pattern = "src/**" and path = "node_modules/") + + // FIXME: this is a bit too restrictive, and fails to consider cases where the + // gloabally set include pattern refers to a file than a directory + // (eg, global pattern = "**.go" and path = "modules/git") + exprMatched := false + for _, expr := range setting.Indexer.IncludePatterns { + if expr.Match(opts.Filename) { + files = append(files, ":(literal)"+opts.Filename) + exprMatched = true + break + } + } + if !exprMatched { + log.Warn("git-grep: filepath %s does not match any include pattern", opts.Filename) + } + } else if len(opts.Filename) > 0 { + // if the path is only set we just include results that matches it + files = append(files, ":(literal)"+opts.Filename) + } else { + // otherwise if global include patterns are set include results that strictly match them + for _, expr := range setting.Indexer.IncludePatterns { + files = append(files, ":"+expr.Pattern()) + } } for _, expr := range setting.Indexer.ExcludePatterns { files = append(files, ":^"+expr.Pattern()) diff --git a/modules/git/grep_test.go b/modules/git/grep_test.go index 835f441b19..c40b33664a 100644 --- a/modules/git/grep_test.go +++ b/modules/git/grep_test.go @@ -89,6 +89,20 @@ func TestGrepSearch(t *testing.T) { }, }, res) + res, err = GrepSearch(context.Background(), repo, "world", GrepOptions{ + MatchesPerFile: 1, + Filename: "java-hello/", + }) + require.NoError(t, err) + assert.Equal(t, []*GrepResult{ + { + Filename: "java-hello/main.java", + LineNumbers: []int{1}, + LineCodes: []string{"public class HelloWorld"}, + HighlightedRanges: [][3]int{{0, 18, 23}}, + }, + }, res) + res, err = GrepSearch(context.Background(), repo, "no-such-content", GrepOptions{}) require.NoError(t, err) assert.Empty(t, res) diff --git a/modules/indexer/code/bleve/bleve.go b/modules/indexer/code/bleve/bleve.go index ad854094e5..9dc915499b 100644 --- a/modules/indexer/code/bleve/bleve.go +++ b/modules/indexer/code/bleve/bleve.go @@ -17,6 +17,7 @@ import ( "code.gitea.io/gitea/modules/charset" "code.gitea.io/gitea/modules/git" "code.gitea.io/gitea/modules/gitrepo" + tokenizer_hierarchy "code.gitea.io/gitea/modules/indexer/code/bleve/tokenizer/hierarchy" "code.gitea.io/gitea/modules/indexer/code/internal" indexer_internal "code.gitea.io/gitea/modules/indexer/internal" inner_bleve "code.gitea.io/gitea/modules/indexer/internal/bleve" @@ -56,6 +57,7 @@ func addUnicodeNormalizeTokenFilter(m *mapping.IndexMappingImpl) error { type RepoIndexerData struct { RepoID int64 CommitID string + Filename string Content string Language string UpdatedAt time.Time @@ -69,7 +71,8 @@ func (d *RepoIndexerData) Type() string { const ( repoIndexerAnalyzer = "repoIndexerAnalyzer" repoIndexerDocType = "repoIndexerDocType" - repoIndexerLatestVersion = 6 + pathHierarchyAnalyzer = "pathHierarchyAnalyzer" + repoIndexerLatestVersion = 7 ) // generateBleveIndexMapping generates a bleve index mapping for the repo indexer @@ -89,6 +92,11 @@ func generateBleveIndexMapping() (mapping.IndexMapping, error) { docMapping.AddFieldMappingsAt("Language", termFieldMapping) docMapping.AddFieldMappingsAt("CommitID", termFieldMapping) + pathFieldMapping := bleve.NewTextFieldMapping() + pathFieldMapping.IncludeInAll = false + pathFieldMapping.Analyzer = pathHierarchyAnalyzer + docMapping.AddFieldMappingsAt("Filename", pathFieldMapping) + timeFieldMapping := bleve.NewDateTimeFieldMapping() timeFieldMapping.IncludeInAll = false docMapping.AddFieldMappingsAt("UpdatedAt", timeFieldMapping) @@ -103,6 +111,13 @@ func generateBleveIndexMapping() (mapping.IndexMapping, error) { "token_filters": []string{unicodeNormalizeName, camelcase.Name, lowercase.Name}, }); err != nil { return nil, err + } else if err := mapping.AddCustomAnalyzer(pathHierarchyAnalyzer, map[string]any{ + "type": analyzer_custom.Name, + "char_filters": []string{}, + "tokenizer": tokenizer_hierarchy.Name, + "token_filters": []string{unicodeNormalizeName}, + }); err != nil { + return nil, err } mapping.DefaultAnalyzer = repoIndexerAnalyzer mapping.AddDocumentMapping(repoIndexerDocType, docMapping) @@ -178,6 +193,7 @@ func (b *Indexer) addUpdate(ctx context.Context, batchWriter git.WriteCloserErro return batch.Index(id, &RepoIndexerData{ RepoID: repo.ID, CommitID: commitSha, + Filename: update.Filename, Content: string(charset.ToUTF8DropErrors(fileContents, charset.ConvertOpts{})), Language: analyze.GetCodeLanguage(update.Filename, fileContents), UpdatedAt: time.Now().UTC(), @@ -266,22 +282,30 @@ func (b *Indexer) Search(ctx context.Context, opts *internal.SearchOptions) (int indexerQuery = keywordQuery } + opts.Filename = strings.Trim(opts.Filename, "/") + if len(opts.Filename) > 0 { + // we use a keyword analyzer for the query than path hierarchy analyzer + // to match only the exact path + // eg, a query for modules/indexer/code + // should not provide results for modules/ nor modules/indexer + indexerQuery = bleve.NewConjunctionQuery( + indexerQuery, + inner_bleve.MatchQuery(opts.Filename, "Filename", analyzer_keyword.Name, 0), + ) + } + // Save for reuse without language filter facetQuery := indexerQuery if len(opts.Language) > 0 { - languageQuery := bleve.NewMatchQuery(opts.Language) - languageQuery.FieldVal = "Language" - languageQuery.Analyzer = analyzer_keyword.Name - indexerQuery = bleve.NewConjunctionQuery( indexerQuery, - languageQuery, + inner_bleve.MatchQuery(opts.Language, "Language", analyzer_keyword.Name, 0), ) } from, pageSize := opts.GetSkipTake() searchRequest := bleve.NewSearchRequestOptions(indexerQuery, pageSize, from, false) - searchRequest.Fields = []string{"Content", "RepoID", "Language", "CommitID", "UpdatedAt"} + searchRequest.Fields = []string{"Content", "RepoID", "Filename", "Language", "CommitID", "UpdatedAt"} searchRequest.IncludeLocations = true if len(opts.Language) == 0 { @@ -320,7 +344,7 @@ func (b *Indexer) Search(ctx context.Context, opts *internal.SearchOptions) (int RepoID: int64(hit.Fields["RepoID"].(float64)), StartIndex: startIndex, EndIndex: endIndex, - Filename: internal.FilenameOfIndexerID(hit.ID), + Filename: hit.Fields["Filename"].(string), Content: hit.Fields["Content"].(string), CommitID: hit.Fields["CommitID"].(string), UpdatedUnix: updatedUnix, @@ -333,7 +357,7 @@ func (b *Indexer) Search(ctx context.Context, opts *internal.SearchOptions) (int if len(opts.Language) > 0 { // Use separate query to go get all language counts facetRequest := bleve.NewSearchRequestOptions(facetQuery, 1, 0, false) - facetRequest.Fields = []string{"Content", "RepoID", "Language", "CommitID", "UpdatedAt"} + facetRequest.Fields = []string{"Content", "RepoID", "Filename", "Language", "CommitID", "UpdatedAt"} facetRequest.IncludeLocations = true facetRequest.AddFacet("languages", bleve.NewFacetRequest("Language", 10)) diff --git a/modules/indexer/code/bleve/tokenizer/hierarchy/hierarchy.go b/modules/indexer/code/bleve/tokenizer/hierarchy/hierarchy.go new file mode 100644 index 0000000000..4cb6a9b038 --- /dev/null +++ b/modules/indexer/code/bleve/tokenizer/hierarchy/hierarchy.go @@ -0,0 +1,69 @@ +// Copyright 2024 The Forgejo Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package hierarchy + +import ( + "bytes" + + "github.com/blevesearch/bleve/v2/analysis" + "github.com/blevesearch/bleve/v2/registry" +) + +const Name = "path_hierarchy" + +type PathHierarchyTokenizer struct{} + +// Similar to elastic's path_hierarchy tokenizer +// This tokenizes a given path into all the possible hierarchies +// For example, +// modules/indexer/code/search.go => +// +// modules/ +// modules/indexer +// modules/indexer/code +// modules/indexer/code/search.go +func (t *PathHierarchyTokenizer) Tokenize(input []byte) analysis.TokenStream { + // trim any extra slashes + input = bytes.Trim(input, "/") + + // zero allocations until the nested directories exceed a depth of 8 (which is unlikely) + rv := make(analysis.TokenStream, 0, 8) + count, off := 1, 0 + + // iterate till all directory seperators + for i := bytes.IndexRune(input[off:], '/'); i != -1; i = bytes.IndexRune(input[off:], '/') { + // the index is relative to input[offest...] + // add this index to the accumlated offset to get the index of the current seperator in input[0...] + off += i + rv = append(rv, &analysis.Token{ + Term: input[:off], // take the slice, input[0...index of seperator] + Start: 0, + End: off, + Position: count, + Type: analysis.AlphaNumeric, + }) + // increment the offset after considering the seperator + off++ + count++ + } + + // the entire file path should always be the last token + rv = append(rv, &analysis.Token{ + Term: input, + Start: 0, + End: len(input), + Position: count, + Type: analysis.AlphaNumeric, + }) + + return rv +} + +func TokenizerConstructor(config map[string]any, cache *registry.Cache) (analysis.Tokenizer, error) { + return &PathHierarchyTokenizer{}, nil +} + +func init() { + registry.RegisterTokenizer(Name, TokenizerConstructor) +} diff --git a/modules/indexer/code/bleve/tokenizer/hierarchy/hierarchy_test.go b/modules/indexer/code/bleve/tokenizer/hierarchy/hierarchy_test.go new file mode 100644 index 0000000000..0ca3c2941d --- /dev/null +++ b/modules/indexer/code/bleve/tokenizer/hierarchy/hierarchy_test.go @@ -0,0 +1,59 @@ +// Copyright 2024 The Forgejo Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package hierarchy + +import ( + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestIndexerBleveHierarchyTokenizer(t *testing.T) { + tokenizer := &PathHierarchyTokenizer{} + keywords := []struct { + Term string + Results []string + }{ + { + Term: "modules/indexer/code/search.go", + Results: []string{ + "modules", + "modules/indexer", + "modules/indexer/code", + "modules/indexer/code/search.go", + }, + }, + { + Term: "/tmp/forgejo/", + Results: []string{ + "tmp", + "tmp/forgejo", + }, + }, + { + Term: "a/b/c/d/e/f/g/h/i/j", + Results: []string{ + "a", + "a/b", + "a/b/c", + "a/b/c/d", + "a/b/c/d/e", + "a/b/c/d/e/f", + "a/b/c/d/e/f/g", + "a/b/c/d/e/f/g/h", + "a/b/c/d/e/f/g/h/i", + "a/b/c/d/e/f/g/h/i/j", + }, + }, + } + + for _, kw := range keywords { + tokens := tokenizer.Tokenize([]byte(kw.Term)) + assert.Len(t, tokens, len(kw.Results)) + for i, token := range tokens { + assert.Equal(t, i+1, token.Position) + assert.Equal(t, kw.Results[i], string(token.Term)) + } + } +} diff --git a/modules/indexer/code/elasticsearch/elasticsearch.go b/modules/indexer/code/elasticsearch/elasticsearch.go index 311c5fe735..ad58615b30 100644 --- a/modules/indexer/code/elasticsearch/elasticsearch.go +++ b/modules/indexer/code/elasticsearch/elasticsearch.go @@ -30,7 +30,7 @@ import ( ) const ( - esRepoIndexerLatestVersion = 1 + esRepoIndexerLatestVersion = 2 // multi-match-types, currently only 2 types are used // Reference: https://www.elastic.co/guide/en/elasticsearch/reference/7.0/query-dsl-multi-match-query.html#multi-match-types esMultiMatchTypeBestFields = "best_fields" @@ -57,6 +57,21 @@ func NewIndexer(url, indexerName string) *Indexer { const ( defaultMapping = `{ + "settings": { + "analysis": { + "analyzer": { + "custom_path_tree": { + "tokenizer": "custom_hierarchy" + } + }, + "tokenizer": { + "custom_hierarchy": { + "type": "path_hierarchy", + "delimiter": "/" + } + } + } + }, "mappings": { "properties": { "repo_id": { @@ -72,6 +87,15 @@ const ( "type": "keyword", "index": true }, + "filename": { + "type": "text", + "fields": { + "tree": { + "type": "text", + "analyzer": "custom_path_tree" + } + } + }, "language": { "type": "keyword", "index": true @@ -138,6 +162,7 @@ func (b *Indexer) addUpdate(ctx context.Context, batchWriter git.WriteCloserErro "repo_id": repo.ID, "content": string(charset.ToUTF8DropErrors(fileContents, charset.ConvertOpts{})), "commit_id": sha, + "filename": update.Filename, "language": analyze.GetCodeLanguage(update.Filename, fileContents), "updated_at": timeutil.TimeStampNow(), }), @@ -267,7 +292,6 @@ func convertResult(searchResult *elastic.SearchResult, kw string, pageSize int) panic(fmt.Sprintf("2===%#v", hit.Highlight)) } - repoID, fileName := internal.ParseIndexerID(hit.Id) res := make(map[string]any) if err := json.Unmarshal(hit.Source, &res); err != nil { return 0, nil, nil, err @@ -276,8 +300,8 @@ func convertResult(searchResult *elastic.SearchResult, kw string, pageSize int) language := res["language"].(string) hits = append(hits, &internal.SearchResult{ - RepoID: repoID, - Filename: fileName, + RepoID: int64(res["repo_id"].(float64)), + Filename: res["filename"].(string), CommitID: res["commit_id"].(string), Content: res["content"].(string), UpdatedUnix: timeutil.TimeStamp(res["updated_at"].(float64)), @@ -326,6 +350,9 @@ func (b *Indexer) Search(ctx context.Context, opts *internal.SearchOptions) (int repoQuery := elastic.NewTermsQuery("repo_id", repoStrs...) query = query.Must(repoQuery) } + if len(opts.Filename) > 0 { + query = query.Filter(elastic.NewTermsQuery("filename.tree", opts.Filename)) + } var ( start, pageSize = opts.GetSkipTake() diff --git a/modules/indexer/code/indexer_test.go b/modules/indexer/code/indexer_test.go index d474eba174..9ef16f3412 100644 --- a/modules/indexer/code/indexer_test.go +++ b/modules/indexer/code/indexer_test.go @@ -34,10 +34,11 @@ func testIndexer(name string, t *testing.T, indexer internal.Indexer) { err := index(git.DefaultContext, indexer, repoID) require.NoError(t, err) keywords := []struct { - RepoIDs []int64 - Keyword string - IDs []int64 - Langs int + RepoIDs []int64 + Keyword string + IDs []int64 + Langs int + Filename string }{ { RepoIDs: nil, @@ -51,6 +52,20 @@ func testIndexer(name string, t *testing.T, indexer internal.Indexer) { IDs: []int64{}, Langs: 0, }, + { + RepoIDs: nil, + Keyword: "Description", + IDs: []int64{}, + Langs: 0, + Filename: "NOT-README.md", + }, + { + RepoIDs: nil, + Keyword: "Description", + IDs: []int64{repoID}, + Langs: 1, + Filename: "README.md", + }, { RepoIDs: nil, Keyword: "Description for", @@ -86,6 +101,7 @@ func testIndexer(name string, t *testing.T, indexer internal.Indexer) { Page: 1, PageSize: 10, }, + Filename: kw.Filename, IsKeywordFuzzy: true, }) require.NoError(t, err) diff --git a/modules/indexer/code/internal/indexer.go b/modules/indexer/code/internal/indexer.go index c259fcd26e..748c9e1bf9 100644 --- a/modules/indexer/code/internal/indexer.go +++ b/modules/indexer/code/internal/indexer.go @@ -24,6 +24,7 @@ type SearchOptions struct { RepoIDs []int64 Keyword string Language string + Filename string IsKeywordFuzzy bool diff --git a/modules/indexer/code/internal/util.go b/modules/indexer/code/internal/util.go index 689c4f4584..1c999fda7d 100644 --- a/modules/indexer/code/internal/util.go +++ b/modules/indexer/code/internal/util.go @@ -3,30 +3,8 @@ package internal -import ( - "strings" - - "code.gitea.io/gitea/modules/indexer/internal" - "code.gitea.io/gitea/modules/log" -) +import "code.gitea.io/gitea/modules/indexer/internal" func FilenameIndexerID(repoID int64, filename string) string { return internal.Base36(repoID) + "_" + filename } - -func ParseIndexerID(indexerID string) (int64, string) { - index := strings.IndexByte(indexerID, '_') - if index == -1 { - log.Error("Unexpected ID in repo indexer: %s", indexerID) - } - repoID, _ := internal.ParseBase36(indexerID[:index]) - return repoID, indexerID[index+1:] -} - -func FilenameOfIndexerID(indexerID string) string { - index := strings.IndexByte(indexerID, '_') - if index == -1 { - log.Error("Unexpected ID in repo indexer: %s", indexerID) - } - return indexerID[index+1:] -} diff --git a/modules/indexer/code/search.go b/modules/indexer/code/search.go index f45907ad90..9f7aa2db60 100644 --- a/modules/indexer/code/search.go +++ b/modules/indexer/code/search.go @@ -35,6 +35,8 @@ type SearchResultLanguages = internal.SearchResultLanguages type SearchOptions = internal.SearchOptions +var CodeSearchOptions = [2]string{"exact", "fuzzy"} + func indices(content string, selectionStartIndex, selectionEndIndex int) (int, int) { startIndex := selectionStartIndex numLinesBefore := 0 diff --git a/routers/web/explore/code.go b/routers/web/explore/code.go index 7992517ad4..76238e80fb 100644 --- a/routers/web/explore/code.go +++ b/routers/web/explore/code.go @@ -35,6 +35,7 @@ func Code(ctx *context.Context) { language := ctx.FormTrim("l") keyword := ctx.FormTrim("q") + path := ctx.FormTrim("path") isFuzzy := ctx.FormOptionalBool("fuzzy").ValueOrDefault(true) if mode := ctx.FormTrim("mode"); len(mode) > 0 { @@ -91,6 +92,7 @@ func Code(ctx *context.Context) { Keyword: keyword, IsKeywordFuzzy: isFuzzy, Language: language, + Filename: path, Paginator: &db.ListOptions{ Page: page, PageSize: setting.UI.RepoSearchPagingNum, diff --git a/routers/web/repo/search.go b/routers/web/repo/search.go index 863a279af0..f2264360ec 100644 --- a/routers/web/repo/search.go +++ b/routers/web/repo/search.go @@ -54,6 +54,7 @@ func Search(ctx *context.Context) { language := ctx.FormTrim("l") keyword := ctx.FormTrim("q") + path := ctx.FormTrim("path") mode := ExactSearchMode if modeStr := ctx.FormString("mode"); len(modeStr) > 0 { mode = searchModeFromString(modeStr) @@ -63,6 +64,7 @@ func Search(ctx *context.Context) { ctx.Data["Keyword"] = keyword ctx.Data["Language"] = language + ctx.Data["CodeSearchPath"] = path ctx.Data["CodeSearchMode"] = mode.String() ctx.Data["PageIsViewCode"] = true @@ -86,6 +88,7 @@ func Search(ctx *context.Context) { Keyword: keyword, IsKeywordFuzzy: mode == FuzzySearchMode, Language: language, + Filename: path, Paginator: &db.ListOptions{ Page: page, PageSize: setting.UI.RepoSearchPagingNum, @@ -100,11 +103,12 @@ func Search(ctx *context.Context) { } else { ctx.Data["CodeIndexerUnavailable"] = !code_indexer.IsAvailable(ctx) } - ctx.Data["CodeSearchOptions"] = []string{"exact", "fuzzy"} + ctx.Data["CodeSearchOptions"] = code_indexer.CodeSearchOptions } else { grepOpt := git.GrepOptions{ ContextLineNumber: 1, RefName: ctx.Repo.RefName, + Filename: path, } switch mode { case FuzzySearchMode: @@ -130,10 +134,12 @@ func Search(ctx *context.Context) { // UpdatedUnix: not supported yet // Language: not supported yet // Color: not supported yet - Lines: code_indexer.HighlightSearchResultCode(r.Filename, r.LineNumbers, r.HighlightedRanges, strings.Join(r.LineCodes, "\n")), + Lines: code_indexer.HighlightSearchResultCode( + r.Filename, r.LineNumbers, r.HighlightedRanges, + strings.Join(r.LineCodes, "\n")), }) } - ctx.Data["CodeSearchOptions"] = []string{"exact", "union", "regexp"} + ctx.Data["CodeSearchOptions"] = git.GrepSearchOptions } ctx.Data["CodeIndexerDisabled"] = !setting.Indexer.RepoIndexerEnabled diff --git a/routers/web/repo/view.go b/routers/web/repo/view.go index e177c81902..fd8c1da058 100644 --- a/routers/web/repo/view.go +++ b/routers/web/repo/view.go @@ -39,6 +39,7 @@ import ( "code.gitea.io/gitea/modules/git" "code.gitea.io/gitea/modules/gitrepo" "code.gitea.io/gitea/modules/highlight" + code_indexer "code.gitea.io/gitea/modules/indexer/code" "code.gitea.io/gitea/modules/lfs" "code.gitea.io/gitea/modules/log" "code.gitea.io/gitea/modules/markup" @@ -1152,6 +1153,12 @@ PostRecentBranchCheck: ctx.Data["TreeNames"] = treeNames ctx.Data["BranchLink"] = branchLink ctx.Data["CodeIndexerDisabled"] = !setting.Indexer.RepoIndexerEnabled + if setting.Indexer.RepoIndexerEnabled { + ctx.Data["CodeIndexerUnavailable"] = !code_indexer.IsAvailable(ctx) + ctx.Data["CodeSearchOptions"] = code_indexer.CodeSearchOptions + } else { + ctx.Data["CodeSearchOptions"] = git.GrepSearchOptions + } ctx.HTML(http.StatusOK, tplRepoHome) } diff --git a/routers/web/user/code.go b/routers/web/user/code.go index 26e48d1ea6..3e044d7876 100644 --- a/routers/web/user/code.go +++ b/routers/web/user/code.go @@ -39,6 +39,7 @@ func CodeSearch(ctx *context.Context) { language := ctx.FormTrim("l") keyword := ctx.FormTrim("q") + path := ctx.FormTrim("path") isFuzzy := ctx.FormOptionalBool("fuzzy").ValueOrDefault(true) if mode := ctx.FormTrim("mode"); len(mode) > 0 { @@ -88,6 +89,7 @@ func CodeSearch(ctx *context.Context) { Keyword: keyword, IsKeywordFuzzy: isFuzzy, Language: language, + Filename: path, Paginator: &db.ListOptions{ Page: page, PageSize: setting.UI.RepoSearchPagingNum, diff --git a/templates/repo/home.tmpl b/templates/repo/home.tmpl index 9cbdef53ca..7bf4ee4a8e 100644 --- a/templates/repo/home.tmpl +++ b/templates/repo/home.tmpl @@ -11,12 +11,6 @@ {{if $description}}{{$description | RenderCodeBlock}}{{else}}{{ctx.Locale.Tr "repo.no_desc"}}{{end}} {{if .Repository.Website}}{{.Repository.Website}}{{end}} -
-
- - {{template "shared/search/button"}} -
-
{{/* it should match the code in issue-home.js */}} @@ -158,6 +152,22 @@ {{else if .IsBlame}} {{template "repo/blame" .}} {{else}}{{/* IsViewDirectory */}} + {{/* display the search bar only if */}} + {{$isCommit := StringUtils.HasPrefix .BranchNameSubURL "commit"}} + {{if and (not $isCommit) (or .CodeIndexerDisabled (and (not .TagName) (eq .Repository.DefaultBranch .BranchName)))}} + + {{end}} {{template "repo/view_list" .}} {{end}}
diff --git a/templates/shared/search/code/results.tmpl b/templates/shared/search/code/results.tmpl index 98c5430502..5525d0c715 100644 --- a/templates/shared/search/code/results.tmpl +++ b/templates/shared/search/code/results.tmpl @@ -1,7 +1,7 @@ {{else}} + {{if .CodeSearchPath}} +
+ + @ + {{$href := ""}} + {{- range $i, $path := StringUtils.Split .CodeSearchPath "/" -}} + {{if eq $i 0}} + {{$href = $path}} + {{else}} + {{$href = StringUtils.Join (StringUtils.Make $href $path) "/"}} + {{end}} + / + {{$path}} + {{- end -}} + +
+ {{end}} {{if .CodeIndexerDisabled}}

{{ctx.Locale.Tr "search.code_search_by_git_grep"}}

diff --git a/tests/integration/repo_test.go b/tests/integration/repo_test.go index f165cae539..90fc19c193 100644 --- a/tests/integration/repo_test.go +++ b/tests/integration/repo_test.go @@ -1009,16 +1009,29 @@ func TestRepoCodeSearchForm(t *testing.T) { resp := MakeRequest(t, req, http.StatusOK) htmlDoc := NewHTMLParser(t, resp.Body) - action, exists := htmlDoc.doc.Find("form[data-test-tag=codesearch]").Attr("action") + formEl := htmlDoc.doc.Find("form[data-test-tag=codesearch]") + + action, exists := formEl.Attr("action") assert.True(t, exists) - branchSubURL := "/branch/master" - if indexer { assert.NotContains(t, action, branchSubURL) } else { assert.Contains(t, action, branchSubURL) } + + filepath, exists := formEl.Find("input[name=path]").Attr("value") + assert.True(t, exists) + assert.Empty(t, filepath) + + req = NewRequest(t, "GET", "/user2/glob/src/branch/master/x/y") + resp = MakeRequest(t, req, http.StatusOK) + + filepath, exists = NewHTMLParser(t, resp.Body).doc. + Find("form[data-test-tag=codesearch] input[name=path]"). + Attr("value") + assert.True(t, exists) + assert.Equal(t, "x/y", filepath) } t.Run("indexer disabled", func(t *testing.T) { diff --git a/web_src/css/repo.css b/web_src/css/repo.css index 6568a0de03..1b3ee51e91 100644 --- a/web_src/css/repo.css +++ b/web_src/css/repo.css @@ -389,6 +389,11 @@ td .commit-summary { cursor: default; } +.code-search + #repo-files-table { + border-top-left-radius: 0; + border-top-right-radius: 0; +} + .view-raw { display: flex; justify-content: center;