loading up the forgejo repo on tangled to test page performance
0
fork

Configure Feed

Select the types of activity you want to include in your feed.

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 <gusted@noreply.codeberg.org>
Reviewed-by: 0ko <0ko@noreply.codeberg.org>
Co-authored-by: Shiny Nematoda <snematoda.751k2@aleeas.com>
Co-committed-by: Shiny Nematoda <snematoda.751k2@aleeas.com>

authored by

Shiny Nematoda
Shiny Nematoda
and committed by
0ko
ee214cb8 bb88e1da

+342 -61
+35 -7
modules/git/grep.go
··· 36 36 RegExpGrepMode 37 37 ) 38 38 39 + var GrepSearchOptions = [3]string{"exact", "union", "regexp"} 40 + 39 41 type GrepOptions struct { 40 42 RefName string 41 43 MaxResultLimit int 42 44 MatchesPerFile int // >= git 2.38 43 45 ContextLineNumber int 44 46 Mode grepMode 45 - PathSpec []setting.Glob 47 + Filename string 46 48 } 47 49 48 50 func (opts *GrepOptions) ensureDefaults() { ··· 112 114 } 113 115 114 116 // pathspec 115 - files := make([]string, 0, 116 - len(setting.Indexer.IncludePatterns)+ 117 - len(setting.Indexer.ExcludePatterns)+ 118 - len(opts.PathSpec)) 119 - for _, expr := range append(setting.Indexer.IncludePatterns, opts.PathSpec...) { 120 - files = append(files, ":"+expr.Pattern()) 117 + includeLen := len(setting.Indexer.IncludePatterns) 118 + if len(opts.Filename) > 0 { 119 + includeLen = 1 120 + } 121 + files := make([]string, 0, len(setting.Indexer.ExcludePatterns)+includeLen) 122 + if len(opts.Filename) > 0 && len(setting.Indexer.IncludePatterns) > 0 { 123 + // if the both a global include pattern and the per search path is defined 124 + // we only include results where the path matches the globally set pattern 125 + // (eg, global pattern = "src/**" and path = "node_modules/") 126 + 127 + // FIXME: this is a bit too restrictive, and fails to consider cases where the 128 + // gloabally set include pattern refers to a file than a directory 129 + // (eg, global pattern = "**.go" and path = "modules/git") 130 + exprMatched := false 131 + for _, expr := range setting.Indexer.IncludePatterns { 132 + if expr.Match(opts.Filename) { 133 + files = append(files, ":(literal)"+opts.Filename) 134 + exprMatched = true 135 + break 136 + } 137 + } 138 + if !exprMatched { 139 + log.Warn("git-grep: filepath %s does not match any include pattern", opts.Filename) 140 + } 141 + } else if len(opts.Filename) > 0 { 142 + // if the path is only set we just include results that matches it 143 + files = append(files, ":(literal)"+opts.Filename) 144 + } else { 145 + // otherwise if global include patterns are set include results that strictly match them 146 + for _, expr := range setting.Indexer.IncludePatterns { 147 + files = append(files, ":"+expr.Pattern()) 148 + } 121 149 } 122 150 for _, expr := range setting.Indexer.ExcludePatterns { 123 151 files = append(files, ":^"+expr.Pattern())
+14
modules/git/grep_test.go
··· 89 89 }, 90 90 }, res) 91 91 92 + res, err = GrepSearch(context.Background(), repo, "world", GrepOptions{ 93 + MatchesPerFile: 1, 94 + Filename: "java-hello/", 95 + }) 96 + require.NoError(t, err) 97 + assert.Equal(t, []*GrepResult{ 98 + { 99 + Filename: "java-hello/main.java", 100 + LineNumbers: []int{1}, 101 + LineCodes: []string{"public class HelloWorld"}, 102 + HighlightedRanges: [][3]int{{0, 18, 23}}, 103 + }, 104 + }, res) 105 + 92 106 res, err = GrepSearch(context.Background(), repo, "no-such-content", GrepOptions{}) 93 107 require.NoError(t, err) 94 108 assert.Empty(t, res)
+33 -9
modules/indexer/code/bleve/bleve.go
··· 17 17 "code.gitea.io/gitea/modules/charset" 18 18 "code.gitea.io/gitea/modules/git" 19 19 "code.gitea.io/gitea/modules/gitrepo" 20 + tokenizer_hierarchy "code.gitea.io/gitea/modules/indexer/code/bleve/tokenizer/hierarchy" 20 21 "code.gitea.io/gitea/modules/indexer/code/internal" 21 22 indexer_internal "code.gitea.io/gitea/modules/indexer/internal" 22 23 inner_bleve "code.gitea.io/gitea/modules/indexer/internal/bleve" ··· 56 57 type RepoIndexerData struct { 57 58 RepoID int64 58 59 CommitID string 60 + Filename string 59 61 Content string 60 62 Language string 61 63 UpdatedAt time.Time ··· 69 71 const ( 70 72 repoIndexerAnalyzer = "repoIndexerAnalyzer" 71 73 repoIndexerDocType = "repoIndexerDocType" 72 - repoIndexerLatestVersion = 6 74 + pathHierarchyAnalyzer = "pathHierarchyAnalyzer" 75 + repoIndexerLatestVersion = 7 73 76 ) 74 77 75 78 // generateBleveIndexMapping generates a bleve index mapping for the repo indexer ··· 89 92 docMapping.AddFieldMappingsAt("Language", termFieldMapping) 90 93 docMapping.AddFieldMappingsAt("CommitID", termFieldMapping) 91 94 95 + pathFieldMapping := bleve.NewTextFieldMapping() 96 + pathFieldMapping.IncludeInAll = false 97 + pathFieldMapping.Analyzer = pathHierarchyAnalyzer 98 + docMapping.AddFieldMappingsAt("Filename", pathFieldMapping) 99 + 92 100 timeFieldMapping := bleve.NewDateTimeFieldMapping() 93 101 timeFieldMapping.IncludeInAll = false 94 102 docMapping.AddFieldMappingsAt("UpdatedAt", timeFieldMapping) ··· 103 111 "token_filters": []string{unicodeNormalizeName, camelcase.Name, lowercase.Name}, 104 112 }); err != nil { 105 113 return nil, err 114 + } else if err := mapping.AddCustomAnalyzer(pathHierarchyAnalyzer, map[string]any{ 115 + "type": analyzer_custom.Name, 116 + "char_filters": []string{}, 117 + "tokenizer": tokenizer_hierarchy.Name, 118 + "token_filters": []string{unicodeNormalizeName}, 119 + }); err != nil { 120 + return nil, err 106 121 } 107 122 mapping.DefaultAnalyzer = repoIndexerAnalyzer 108 123 mapping.AddDocumentMapping(repoIndexerDocType, docMapping) ··· 178 193 return batch.Index(id, &RepoIndexerData{ 179 194 RepoID: repo.ID, 180 195 CommitID: commitSha, 196 + Filename: update.Filename, 181 197 Content: string(charset.ToUTF8DropErrors(fileContents, charset.ConvertOpts{})), 182 198 Language: analyze.GetCodeLanguage(update.Filename, fileContents), 183 199 UpdatedAt: time.Now().UTC(), ··· 266 282 indexerQuery = keywordQuery 267 283 } 268 284 285 + opts.Filename = strings.Trim(opts.Filename, "/") 286 + if len(opts.Filename) > 0 { 287 + // we use a keyword analyzer for the query than path hierarchy analyzer 288 + // to match only the exact path 289 + // eg, a query for modules/indexer/code 290 + // should not provide results for modules/ nor modules/indexer 291 + indexerQuery = bleve.NewConjunctionQuery( 292 + indexerQuery, 293 + inner_bleve.MatchQuery(opts.Filename, "Filename", analyzer_keyword.Name, 0), 294 + ) 295 + } 296 + 269 297 // Save for reuse without language filter 270 298 facetQuery := indexerQuery 271 299 if len(opts.Language) > 0 { 272 - languageQuery := bleve.NewMatchQuery(opts.Language) 273 - languageQuery.FieldVal = "Language" 274 - languageQuery.Analyzer = analyzer_keyword.Name 275 - 276 300 indexerQuery = bleve.NewConjunctionQuery( 277 301 indexerQuery, 278 - languageQuery, 302 + inner_bleve.MatchQuery(opts.Language, "Language", analyzer_keyword.Name, 0), 279 303 ) 280 304 } 281 305 282 306 from, pageSize := opts.GetSkipTake() 283 307 searchRequest := bleve.NewSearchRequestOptions(indexerQuery, pageSize, from, false) 284 - searchRequest.Fields = []string{"Content", "RepoID", "Language", "CommitID", "UpdatedAt"} 308 + searchRequest.Fields = []string{"Content", "RepoID", "Filename", "Language", "CommitID", "UpdatedAt"} 285 309 searchRequest.IncludeLocations = true 286 310 287 311 if len(opts.Language) == 0 { ··· 320 344 RepoID: int64(hit.Fields["RepoID"].(float64)), 321 345 StartIndex: startIndex, 322 346 EndIndex: endIndex, 323 - Filename: internal.FilenameOfIndexerID(hit.ID), 347 + Filename: hit.Fields["Filename"].(string), 324 348 Content: hit.Fields["Content"].(string), 325 349 CommitID: hit.Fields["CommitID"].(string), 326 350 UpdatedUnix: updatedUnix, ··· 333 357 if len(opts.Language) > 0 { 334 358 // Use separate query to go get all language counts 335 359 facetRequest := bleve.NewSearchRequestOptions(facetQuery, 1, 0, false) 336 - facetRequest.Fields = []string{"Content", "RepoID", "Language", "CommitID", "UpdatedAt"} 360 + facetRequest.Fields = []string{"Content", "RepoID", "Filename", "Language", "CommitID", "UpdatedAt"} 337 361 facetRequest.IncludeLocations = true 338 362 facetRequest.AddFacet("languages", bleve.NewFacetRequest("Language", 10)) 339 363
+69
modules/indexer/code/bleve/tokenizer/hierarchy/hierarchy.go
··· 1 + // Copyright 2024 The Forgejo Authors. All rights reserved. 2 + // SPDX-License-Identifier: MIT 3 + 4 + package hierarchy 5 + 6 + import ( 7 + "bytes" 8 + 9 + "github.com/blevesearch/bleve/v2/analysis" 10 + "github.com/blevesearch/bleve/v2/registry" 11 + ) 12 + 13 + const Name = "path_hierarchy" 14 + 15 + type PathHierarchyTokenizer struct{} 16 + 17 + // Similar to elastic's path_hierarchy tokenizer 18 + // This tokenizes a given path into all the possible hierarchies 19 + // For example, 20 + // modules/indexer/code/search.go => 21 + // 22 + // modules/ 23 + // modules/indexer 24 + // modules/indexer/code 25 + // modules/indexer/code/search.go 26 + func (t *PathHierarchyTokenizer) Tokenize(input []byte) analysis.TokenStream { 27 + // trim any extra slashes 28 + input = bytes.Trim(input, "/") 29 + 30 + // zero allocations until the nested directories exceed a depth of 8 (which is unlikely) 31 + rv := make(analysis.TokenStream, 0, 8) 32 + count, off := 1, 0 33 + 34 + // iterate till all directory seperators 35 + for i := bytes.IndexRune(input[off:], '/'); i != -1; i = bytes.IndexRune(input[off:], '/') { 36 + // the index is relative to input[offest...] 37 + // add this index to the accumlated offset to get the index of the current seperator in input[0...] 38 + off += i 39 + rv = append(rv, &analysis.Token{ 40 + Term: input[:off], // take the slice, input[0...index of seperator] 41 + Start: 0, 42 + End: off, 43 + Position: count, 44 + Type: analysis.AlphaNumeric, 45 + }) 46 + // increment the offset after considering the seperator 47 + off++ 48 + count++ 49 + } 50 + 51 + // the entire file path should always be the last token 52 + rv = append(rv, &analysis.Token{ 53 + Term: input, 54 + Start: 0, 55 + End: len(input), 56 + Position: count, 57 + Type: analysis.AlphaNumeric, 58 + }) 59 + 60 + return rv 61 + } 62 + 63 + func TokenizerConstructor(config map[string]any, cache *registry.Cache) (analysis.Tokenizer, error) { 64 + return &PathHierarchyTokenizer{}, nil 65 + } 66 + 67 + func init() { 68 + registry.RegisterTokenizer(Name, TokenizerConstructor) 69 + }
+59
modules/indexer/code/bleve/tokenizer/hierarchy/hierarchy_test.go
··· 1 + // Copyright 2024 The Forgejo Authors. All rights reserved. 2 + // SPDX-License-Identifier: MIT 3 + 4 + package hierarchy 5 + 6 + import ( 7 + "testing" 8 + 9 + "github.com/stretchr/testify/assert" 10 + ) 11 + 12 + func TestIndexerBleveHierarchyTokenizer(t *testing.T) { 13 + tokenizer := &PathHierarchyTokenizer{} 14 + keywords := []struct { 15 + Term string 16 + Results []string 17 + }{ 18 + { 19 + Term: "modules/indexer/code/search.go", 20 + Results: []string{ 21 + "modules", 22 + "modules/indexer", 23 + "modules/indexer/code", 24 + "modules/indexer/code/search.go", 25 + }, 26 + }, 27 + { 28 + Term: "/tmp/forgejo/", 29 + Results: []string{ 30 + "tmp", 31 + "tmp/forgejo", 32 + }, 33 + }, 34 + { 35 + Term: "a/b/c/d/e/f/g/h/i/j", 36 + Results: []string{ 37 + "a", 38 + "a/b", 39 + "a/b/c", 40 + "a/b/c/d", 41 + "a/b/c/d/e", 42 + "a/b/c/d/e/f", 43 + "a/b/c/d/e/f/g", 44 + "a/b/c/d/e/f/g/h", 45 + "a/b/c/d/e/f/g/h/i", 46 + "a/b/c/d/e/f/g/h/i/j", 47 + }, 48 + }, 49 + } 50 + 51 + for _, kw := range keywords { 52 + tokens := tokenizer.Tokenize([]byte(kw.Term)) 53 + assert.Len(t, tokens, len(kw.Results)) 54 + for i, token := range tokens { 55 + assert.Equal(t, i+1, token.Position) 56 + assert.Equal(t, kw.Results[i], string(token.Term)) 57 + } 58 + } 59 + }
+31 -4
modules/indexer/code/elasticsearch/elasticsearch.go
··· 30 30 ) 31 31 32 32 const ( 33 - esRepoIndexerLatestVersion = 1 33 + esRepoIndexerLatestVersion = 2 34 34 // multi-match-types, currently only 2 types are used 35 35 // Reference: https://www.elastic.co/guide/en/elasticsearch/reference/7.0/query-dsl-multi-match-query.html#multi-match-types 36 36 esMultiMatchTypeBestFields = "best_fields" ··· 57 57 58 58 const ( 59 59 defaultMapping = `{ 60 + "settings": { 61 + "analysis": { 62 + "analyzer": { 63 + "custom_path_tree": { 64 + "tokenizer": "custom_hierarchy" 65 + } 66 + }, 67 + "tokenizer": { 68 + "custom_hierarchy": { 69 + "type": "path_hierarchy", 70 + "delimiter": "/" 71 + } 72 + } 73 + } 74 + }, 60 75 "mappings": { 61 76 "properties": { 62 77 "repo_id": { ··· 72 87 "type": "keyword", 73 88 "index": true 74 89 }, 90 + "filename": { 91 + "type": "text", 92 + "fields": { 93 + "tree": { 94 + "type": "text", 95 + "analyzer": "custom_path_tree" 96 + } 97 + } 98 + }, 75 99 "language": { 76 100 "type": "keyword", 77 101 "index": true ··· 138 162 "repo_id": repo.ID, 139 163 "content": string(charset.ToUTF8DropErrors(fileContents, charset.ConvertOpts{})), 140 164 "commit_id": sha, 165 + "filename": update.Filename, 141 166 "language": analyze.GetCodeLanguage(update.Filename, fileContents), 142 167 "updated_at": timeutil.TimeStampNow(), 143 168 }), ··· 267 292 panic(fmt.Sprintf("2===%#v", hit.Highlight)) 268 293 } 269 294 270 - repoID, fileName := internal.ParseIndexerID(hit.Id) 271 295 res := make(map[string]any) 272 296 if err := json.Unmarshal(hit.Source, &res); err != nil { 273 297 return 0, nil, nil, err ··· 276 300 language := res["language"].(string) 277 301 278 302 hits = append(hits, &internal.SearchResult{ 279 - RepoID: repoID, 280 - Filename: fileName, 303 + RepoID: int64(res["repo_id"].(float64)), 304 + Filename: res["filename"].(string), 281 305 CommitID: res["commit_id"].(string), 282 306 Content: res["content"].(string), 283 307 UpdatedUnix: timeutil.TimeStamp(res["updated_at"].(float64)), ··· 325 349 } 326 350 repoQuery := elastic.NewTermsQuery("repo_id", repoStrs...) 327 351 query = query.Must(repoQuery) 352 + } 353 + if len(opts.Filename) > 0 { 354 + query = query.Filter(elastic.NewTermsQuery("filename.tree", opts.Filename)) 328 355 } 329 356 330 357 var (
+20 -4
modules/indexer/code/indexer_test.go
··· 34 34 err := index(git.DefaultContext, indexer, repoID) 35 35 require.NoError(t, err) 36 36 keywords := []struct { 37 - RepoIDs []int64 38 - Keyword string 39 - IDs []int64 40 - Langs int 37 + RepoIDs []int64 38 + Keyword string 39 + IDs []int64 40 + Langs int 41 + Filename string 41 42 }{ 42 43 { 43 44 RepoIDs: nil, ··· 50 51 Keyword: "Description", 51 52 IDs: []int64{}, 52 53 Langs: 0, 54 + }, 55 + { 56 + RepoIDs: nil, 57 + Keyword: "Description", 58 + IDs: []int64{}, 59 + Langs: 0, 60 + Filename: "NOT-README.md", 61 + }, 62 + { 63 + RepoIDs: nil, 64 + Keyword: "Description", 65 + IDs: []int64{repoID}, 66 + Langs: 1, 67 + Filename: "README.md", 53 68 }, 54 69 { 55 70 RepoIDs: nil, ··· 86 101 Page: 1, 87 102 PageSize: 10, 88 103 }, 104 + Filename: kw.Filename, 89 105 IsKeywordFuzzy: true, 90 106 }) 91 107 require.NoError(t, err)
+1
modules/indexer/code/internal/indexer.go
··· 24 24 RepoIDs []int64 25 25 Keyword string 26 26 Language string 27 + Filename string 27 28 28 29 IsKeywordFuzzy bool 29 30
+1 -23
modules/indexer/code/internal/util.go
··· 3 3 4 4 package internal 5 5 6 - import ( 7 - "strings" 8 - 9 - "code.gitea.io/gitea/modules/indexer/internal" 10 - "code.gitea.io/gitea/modules/log" 11 - ) 6 + import "code.gitea.io/gitea/modules/indexer/internal" 12 7 13 8 func FilenameIndexerID(repoID int64, filename string) string { 14 9 return internal.Base36(repoID) + "_" + filename 15 10 } 16 - 17 - func ParseIndexerID(indexerID string) (int64, string) { 18 - index := strings.IndexByte(indexerID, '_') 19 - if index == -1 { 20 - log.Error("Unexpected ID in repo indexer: %s", indexerID) 21 - } 22 - repoID, _ := internal.ParseBase36(indexerID[:index]) 23 - return repoID, indexerID[index+1:] 24 - } 25 - 26 - func FilenameOfIndexerID(indexerID string) string { 27 - index := strings.IndexByte(indexerID, '_') 28 - if index == -1 { 29 - log.Error("Unexpected ID in repo indexer: %s", indexerID) 30 - } 31 - return indexerID[index+1:] 32 - }
+2
modules/indexer/code/search.go
··· 35 35 36 36 type SearchOptions = internal.SearchOptions 37 37 38 + var CodeSearchOptions = [2]string{"exact", "fuzzy"} 39 + 38 40 func indices(content string, selectionStartIndex, selectionEndIndex int) (int, int) { 39 41 startIndex := selectionStartIndex 40 42 numLinesBefore := 0
+2
routers/web/explore/code.go
··· 35 35 36 36 language := ctx.FormTrim("l") 37 37 keyword := ctx.FormTrim("q") 38 + path := ctx.FormTrim("path") 38 39 39 40 isFuzzy := ctx.FormOptionalBool("fuzzy").ValueOrDefault(true) 40 41 if mode := ctx.FormTrim("mode"); len(mode) > 0 { ··· 91 92 Keyword: keyword, 92 93 IsKeywordFuzzy: isFuzzy, 93 94 Language: language, 95 + Filename: path, 94 96 Paginator: &db.ListOptions{ 95 97 Page: page, 96 98 PageSize: setting.UI.RepoSearchPagingNum,
+9 -3
routers/web/repo/search.go
··· 54 54 language := ctx.FormTrim("l") 55 55 keyword := ctx.FormTrim("q") 56 56 57 + path := ctx.FormTrim("path") 57 58 mode := ExactSearchMode 58 59 if modeStr := ctx.FormString("mode"); len(modeStr) > 0 { 59 60 mode = searchModeFromString(modeStr) ··· 63 64 64 65 ctx.Data["Keyword"] = keyword 65 66 ctx.Data["Language"] = language 67 + ctx.Data["CodeSearchPath"] = path 66 68 ctx.Data["CodeSearchMode"] = mode.String() 67 69 ctx.Data["PageIsViewCode"] = true 68 70 ··· 86 88 Keyword: keyword, 87 89 IsKeywordFuzzy: mode == FuzzySearchMode, 88 90 Language: language, 91 + Filename: path, 89 92 Paginator: &db.ListOptions{ 90 93 Page: page, 91 94 PageSize: setting.UI.RepoSearchPagingNum, ··· 100 103 } else { 101 104 ctx.Data["CodeIndexerUnavailable"] = !code_indexer.IsAvailable(ctx) 102 105 } 103 - ctx.Data["CodeSearchOptions"] = []string{"exact", "fuzzy"} 106 + ctx.Data["CodeSearchOptions"] = code_indexer.CodeSearchOptions 104 107 } else { 105 108 grepOpt := git.GrepOptions{ 106 109 ContextLineNumber: 1, 107 110 RefName: ctx.Repo.RefName, 111 + Filename: path, 108 112 } 109 113 switch mode { 110 114 case FuzzySearchMode: ··· 130 134 // UpdatedUnix: not supported yet 131 135 // Language: not supported yet 132 136 // Color: not supported yet 133 - Lines: code_indexer.HighlightSearchResultCode(r.Filename, r.LineNumbers, r.HighlightedRanges, strings.Join(r.LineCodes, "\n")), 137 + Lines: code_indexer.HighlightSearchResultCode( 138 + r.Filename, r.LineNumbers, r.HighlightedRanges, 139 + strings.Join(r.LineCodes, "\n")), 134 140 }) 135 141 } 136 - ctx.Data["CodeSearchOptions"] = []string{"exact", "union", "regexp"} 142 + ctx.Data["CodeSearchOptions"] = git.GrepSearchOptions 137 143 } 138 144 139 145 ctx.Data["CodeIndexerDisabled"] = !setting.Indexer.RepoIndexerEnabled
+7
routers/web/repo/view.go
··· 39 39 "code.gitea.io/gitea/modules/git" 40 40 "code.gitea.io/gitea/modules/gitrepo" 41 41 "code.gitea.io/gitea/modules/highlight" 42 + code_indexer "code.gitea.io/gitea/modules/indexer/code" 42 43 "code.gitea.io/gitea/modules/lfs" 43 44 "code.gitea.io/gitea/modules/log" 44 45 "code.gitea.io/gitea/modules/markup" ··· 1152 1153 ctx.Data["TreeNames"] = treeNames 1153 1154 ctx.Data["BranchLink"] = branchLink 1154 1155 ctx.Data["CodeIndexerDisabled"] = !setting.Indexer.RepoIndexerEnabled 1156 + if setting.Indexer.RepoIndexerEnabled { 1157 + ctx.Data["CodeIndexerUnavailable"] = !code_indexer.IsAvailable(ctx) 1158 + ctx.Data["CodeSearchOptions"] = code_indexer.CodeSearchOptions 1159 + } else { 1160 + ctx.Data["CodeSearchOptions"] = git.GrepSearchOptions 1161 + } 1155 1162 ctx.HTML(http.StatusOK, tplRepoHome) 1156 1163 } 1157 1164
+2
routers/web/user/code.go
··· 39 39 40 40 language := ctx.FormTrim("l") 41 41 keyword := ctx.FormTrim("q") 42 + path := ctx.FormTrim("path") 42 43 43 44 isFuzzy := ctx.FormOptionalBool("fuzzy").ValueOrDefault(true) 44 45 if mode := ctx.FormTrim("mode"); len(mode) > 0 { ··· 88 89 Keyword: keyword, 89 90 IsKeywordFuzzy: isFuzzy, 90 91 Language: language, 92 + Filename: path, 91 93 Paginator: &db.ListOptions{ 92 94 Page: page, 93 95 PageSize: setting.UI.RepoSearchPagingNum,
+16 -6
templates/repo/home.tmpl
··· 11 11 {{if $description}}<span class="description">{{$description | RenderCodeBlock}}</span>{{else}}<span class="no-description text-italic">{{ctx.Locale.Tr "repo.no_desc"}}</span>{{end}} 12 12 {{if .Repository.Website}}<a class="link" href="{{.Repository.Website}}">{{.Repository.Website}}</a>{{end}} 13 13 </div> 14 - <form class="ignore-dirty" action="{{.RepoLink}}/search/{{if .CodeIndexerDisabled}}{{.BranchNameSubURL}}{{end}}" method="get" data-test-tag="codesearch"> 15 - <div class="ui small action input"> 16 - <input name="q" value="{{.Keyword}}" placeholder="{{ctx.Locale.Tr "search.code_kind"}}"> 17 - {{template "shared/search/button"}} 18 - </div> 19 - </form> 20 14 </div> 21 15 <div class="tw-flex tw-items-center tw-flex-wrap tw-gap-2 tw-my-2" id="repo-topics"> 22 16 {{/* it should match the code in issue-home.js */}} ··· 158 152 {{else if .IsBlame}} 159 153 {{template "repo/blame" .}} 160 154 {{else}}{{/* IsViewDirectory */}} 155 + {{/* display the search bar only if */}} 156 + {{$isCommit := StringUtils.HasPrefix .BranchNameSubURL "commit"}} 157 + {{if and (not $isCommit) (or .CodeIndexerDisabled (and (not .TagName) (eq .Repository.DefaultBranch .BranchName)))}} 158 + <div class="code-search tw-w-full tw-py-2 tw-px-2 tw-bg-box-header tw-rounded-t tw-border tw-border-secondary tw-border-b-0"> 159 + <form class="ui form ignore-dirty" action="{{.RepoLink}}/search/{{if .CodeIndexerDisabled}}{{.BranchNameSubURL}}{{end}}" method="get" data-test-tag="codesearch"> 160 + <input type="hidden" name="path" value="{{.TreePath | PathEscapeSegments}}"> 161 + {{template "shared/search/combo_multi" 162 + dict 163 + "Value" .Keyword 164 + "Disabled" .CodeIndexerUnavailable 165 + "Placeholder" (ctx.Locale.Tr "search.code_kind") 166 + "Selected" (index .CodeSearchOptions 0) 167 + "Options" .CodeSearchOptions}} 168 + </form> 169 + </div> 170 + {{end}} 161 171 {{template "repo/view_list" .}} 162 172 {{end}} 163 173 </div>
+2 -2
templates/shared/search/code/results.tmpl
··· 1 1 <div class="flex-text-block tw-flex-wrap"> 2 2 {{range $term := .SearchResultLanguages}} 3 3 <a class="ui {{if eq $.Language $term.Language}}primary{{end}} basic label tw-m-0" 4 - href="?q={{$.Keyword}}{{if ne $.Language $term.Language}}&l={{$term.Language}}{{end}}&mode={{$.CodeSearchMode}}"> 4 + href="?q={{$.Keyword}}{{if ne $.Language $term.Language}}&l={{$term.Language}}{{end}}&mode={{$.CodeSearchMode}}&path={{$.CodeSearchPath}}"> 5 5 <i class="color-icon tw-mr-2" style="background-color: {{$term.Color}}"></i> 6 6 {{$term.Language}} 7 7 <div class="detail">{{$term.Count}}</div> ··· 13 13 {{$repo := or $.Repo (index $.RepoMaps .RepoID)}} 14 14 <details class="tw-group diff-file-box diff-box file-content non-diff-file-content repo-search-result" open> 15 15 <summary class="tw-list-none"> 16 - <h4 class="ui top attached header tw-font-normal tw-flex tw-flex-wrap tw-transform-reset"> 16 + <h4 class="ui top attached header tw-font-normal tw-flex tw-items-center tw-flex-wrap tw-transform-reset"> 17 17 <span class="tw-h-4 tw-transition -tw-rotate-90 group-open:tw-rotate-0"> 18 18 {{svg "octicon-chevron-down"}} 19 19 </span>
+18
templates/shared/search/code/search.tmpl
··· 1 1 <form class="ui form ignore-dirty"> 2 + <input type="hidden" name="path" value="{{.CodeSearchPath}}"> 2 3 {{template "shared/search/combo_multi" 3 4 dict 4 5 "Value" .Keyword ··· 14 15 <p>{{ctx.Locale.Tr "search.code_search_unavailable"}}</p> 15 16 </div> 16 17 {{else}} 18 + {{if .CodeSearchPath}} 19 + <div class="tw-mb-4"> 20 + <span class="breadcrumb"> 21 + <a class="section" href="?q={{.Keyword}}&mode={{.CodeSearchMode}}">@</a> 22 + {{$href := ""}} 23 + {{- range $i, $path := StringUtils.Split .CodeSearchPath "/" -}} 24 + {{if eq $i 0}} 25 + {{$href = $path}} 26 + {{else}} 27 + {{$href = StringUtils.Join (StringUtils.Make $href $path) "/"}} 28 + {{end}} 29 + <span class="breadcrumb-divider">/</span> 30 + <span class="section"><a href="?q={{$.Keyword}}&mode={{$.CodeSearchMode}}&path={{$href}}">{{$path}}</a></span> 31 + {{- end -}} 32 + </span> 33 + </div> 34 + {{end}} 17 35 {{if .CodeIndexerDisabled}} 18 36 <div class="ui message" data-test-tag="grep"> 19 37 <p>{{ctx.Locale.Tr "search.code_search_by_git_grep"}}</p>
+16 -3
tests/integration/repo_test.go
··· 1009 1009 resp := MakeRequest(t, req, http.StatusOK) 1010 1010 1011 1011 htmlDoc := NewHTMLParser(t, resp.Body) 1012 - action, exists := htmlDoc.doc.Find("form[data-test-tag=codesearch]").Attr("action") 1012 + formEl := htmlDoc.doc.Find("form[data-test-tag=codesearch]") 1013 + 1014 + action, exists := formEl.Attr("action") 1013 1015 assert.True(t, exists) 1014 - 1015 1016 branchSubURL := "/branch/master" 1016 - 1017 1017 if indexer { 1018 1018 assert.NotContains(t, action, branchSubURL) 1019 1019 } else { 1020 1020 assert.Contains(t, action, branchSubURL) 1021 1021 } 1022 + 1023 + filepath, exists := formEl.Find("input[name=path]").Attr("value") 1024 + assert.True(t, exists) 1025 + assert.Empty(t, filepath) 1026 + 1027 + req = NewRequest(t, "GET", "/user2/glob/src/branch/master/x/y") 1028 + resp = MakeRequest(t, req, http.StatusOK) 1029 + 1030 + filepath, exists = NewHTMLParser(t, resp.Body).doc. 1031 + Find("form[data-test-tag=codesearch] input[name=path]"). 1032 + Attr("value") 1033 + assert.True(t, exists) 1034 + assert.Equal(t, "x/y", filepath) 1022 1035 } 1023 1036 1024 1037 t.Run("indexer disabled", func(t *testing.T) {
+5
web_src/css/repo.css
··· 389 389 cursor: default; 390 390 } 391 391 392 + .code-search + #repo-files-table { 393 + border-top-left-radius: 0; 394 + border-top-right-radius: 0; 395 + } 396 + 392 397 .view-raw { 393 398 display: flex; 394 399 justify-content: center;