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.

Merge pull request '[PORT] Support repo code search without setting up an indexer (gitea#29998)' (#2813) from snematoda/port-git-grep into forgejo

Reviewed-on: https://codeberg.org/forgejo/forgejo/pulls/2813
Reviewed-by: Earl Warren <earl-warren@noreply.codeberg.org>

+285 -277
+6
docs/content/administration/repo-indexer.en-us.md
··· 17 17 18 18 # Repository indexer 19 19 20 + ## Builtin repository code search without indexer 21 + 22 + Users could do repository-level code search without setting up a repository indexer. 23 + The builtin code search is based on the `git grep` command, which is fast and efficient for small repositories. 24 + Better code search support could be achieved by setting up the repository indexer. 25 + 20 26 ## Setting up the repository indexer 21 27 22 28 Gitea can search through the files of the repositories by enabling this function in your [`app.ini`](administration/config-cheat-sheet.md):
+3
docs/content/installation/comparison.en-us.md
··· 87 87 | Git Blame | ✓ | ✘ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | 88 88 | Visual comparison of image changes | ✓ | ✘ | ✓ | ? | ? | ? | ✘ | ✘ | 89 89 90 + - Gitea has builtin repository-level code search 91 + - Better code search support could be achieved by [using a repository indexer](administration/repo-indexer.md) 92 + 90 93 ## Issue Tracker 91 94 92 95 | Feature | Gitea | Gogs | GitHub EE | GitLab CE | GitLab EE | BitBucket | RhodeCode CE | RhodeCode EE |
+2 -3
modules/git/command.go
··· 367 367 error 368 368 Unwrap() error 369 369 Stderr() string 370 - IsExitCode(code int) bool 371 370 } 372 371 373 372 type runStdError struct { ··· 392 391 return r.stderr 393 392 } 394 393 395 - func (r *runStdError) IsExitCode(code int) bool { 394 + func IsErrorExitCode(err error, code int) bool { 396 395 var exitError *exec.ExitError 397 - if errors.As(r.err, &exitError) { 396 + if errors.As(err, &exitError) { 398 397 return exitError.ExitCode() == code 399 398 } 400 399 return false
+4 -4
modules/git/git.go
··· 340 340 341 341 func configSet(key, value string) error { 342 342 stdout, _, err := NewCommand(DefaultContext, "config", "--global", "--get").AddDynamicArguments(key).RunStdString(nil) 343 - if err != nil && !err.IsExitCode(1) { 343 + if err != nil && !IsErrorExitCode(err, 1) { 344 344 return fmt.Errorf("failed to get git config %s, err: %w", key, err) 345 345 } 346 346 ··· 363 363 // already exist 364 364 return nil 365 365 } 366 - if err.IsExitCode(1) { 366 + if IsErrorExitCode(err, 1) { 367 367 // not exist, set new config 368 368 _, _, err = NewCommand(DefaultContext, "config", "--global").AddDynamicArguments(key, value).RunStdString(nil) 369 369 if err != nil { ··· 381 381 // already exist 382 382 return nil 383 383 } 384 - if err.IsExitCode(1) { 384 + if IsErrorExitCode(err, 1) { 385 385 // not exist, add new config 386 386 _, _, err = NewCommand(DefaultContext, "config", "--global", "--add").AddDynamicArguments(key, value).RunStdString(nil) 387 387 if err != nil { ··· 402 402 } 403 403 return nil 404 404 } 405 - if err.IsExitCode(1) { 405 + if IsErrorExitCode(err, 1) { 406 406 // not exist 407 407 return nil 408 408 }
+118
modules/git/grep.go
··· 1 + // Copyright 2024 The Gitea Authors. All rights reserved. 2 + // SPDX-License-Identifier: MIT 3 + 4 + package git 5 + 6 + import ( 7 + "bufio" 8 + "bytes" 9 + "context" 10 + "errors" 11 + "fmt" 12 + "os" 13 + "strconv" 14 + "strings" 15 + 16 + "code.gitea.io/gitea/modules/util" 17 + ) 18 + 19 + type GrepResult struct { 20 + Filename string 21 + LineNumbers []int 22 + LineCodes []string 23 + } 24 + 25 + type GrepOptions struct { 26 + RefName string 27 + MaxResultLimit int 28 + ContextLineNumber int 29 + IsFuzzy bool 30 + } 31 + 32 + func GrepSearch(ctx context.Context, repo *Repository, search string, opts GrepOptions) ([]*GrepResult, error) { 33 + stdoutReader, stdoutWriter, err := os.Pipe() 34 + if err != nil { 35 + return nil, fmt.Errorf("unable to create os pipe to grep: %w", err) 36 + } 37 + defer func() { 38 + _ = stdoutReader.Close() 39 + _ = stdoutWriter.Close() 40 + }() 41 + 42 + /* 43 + The output is like this ( "^@" means \x00): 44 + 45 + HEAD:.air.toml 46 + 6^@bin = "gitea" 47 + 48 + HEAD:.changelog.yml 49 + 2^@repo: go-gitea/gitea 50 + */ 51 + var results []*GrepResult 52 + cmd := NewCommand(ctx, "grep", "--null", "--break", "--heading", "--fixed-strings", "--line-number", "--ignore-case", "--full-name") 53 + cmd.AddOptionValues("--context", fmt.Sprint(opts.ContextLineNumber)) 54 + if opts.IsFuzzy { 55 + words := strings.Fields(search) 56 + for _, word := range words { 57 + cmd.AddOptionValues("-e", strings.TrimLeft(word, "-")) 58 + } 59 + } else { 60 + cmd.AddOptionValues("-e", strings.TrimLeft(search, "-")) 61 + } 62 + cmd.AddDynamicArguments(util.IfZero(opts.RefName, "HEAD")) 63 + opts.MaxResultLimit = util.IfZero(opts.MaxResultLimit, 50) 64 + stderr := bytes.Buffer{} 65 + err = cmd.Run(&RunOpts{ 66 + Dir: repo.Path, 67 + Stdout: stdoutWriter, 68 + Stderr: &stderr, 69 + PipelineFunc: func(ctx context.Context, cancel context.CancelFunc) error { 70 + _ = stdoutWriter.Close() 71 + defer stdoutReader.Close() 72 + 73 + isInBlock := false 74 + scanner := bufio.NewScanner(stdoutReader) 75 + var res *GrepResult 76 + for scanner.Scan() { 77 + line := scanner.Text() 78 + if !isInBlock { 79 + if _ /* ref */, filename, ok := strings.Cut(line, ":"); ok { 80 + isInBlock = true 81 + res = &GrepResult{Filename: filename} 82 + results = append(results, res) 83 + } 84 + continue 85 + } 86 + if line == "" { 87 + if len(results) >= opts.MaxResultLimit { 88 + cancel() 89 + break 90 + } 91 + isInBlock = false 92 + continue 93 + } 94 + if line == "--" { 95 + continue 96 + } 97 + if lineNum, lineCode, ok := strings.Cut(line, "\x00"); ok { 98 + lineNumInt, _ := strconv.Atoi(lineNum) 99 + res.LineNumbers = append(res.LineNumbers, lineNumInt) 100 + res.LineCodes = append(res.LineCodes, lineCode) 101 + } 102 + } 103 + return scanner.Err() 104 + }, 105 + }) 106 + // git grep exits by cancel (killed), usually it is caused by the limit of results 107 + if IsErrorExitCode(err, -1) && stderr.Len() == 0 { 108 + return results, nil 109 + } 110 + // git grep exits with 1 if no results are found 111 + if IsErrorExitCode(err, 1) && stderr.Len() == 0 { 112 + return nil, nil 113 + } 114 + if err != nil && !errors.Is(err, context.Canceled) { 115 + return nil, fmt.Errorf("unable to run git grep: %w, stderr: %s", err, stderr.String()) 116 + } 117 + return results, nil 118 + }
+51
modules/git/grep_test.go
··· 1 + // Copyright 2024 The Gitea Authors. All rights reserved. 2 + // SPDX-License-Identifier: MIT 3 + 4 + package git 5 + 6 + import ( 7 + "context" 8 + "path/filepath" 9 + "testing" 10 + 11 + "github.com/stretchr/testify/assert" 12 + ) 13 + 14 + func TestGrepSearch(t *testing.T) { 15 + repo, err := openRepositoryWithDefaultContext(filepath.Join(testReposDir, "language_stats_repo")) 16 + assert.NoError(t, err) 17 + defer repo.Close() 18 + 19 + res, err := GrepSearch(context.Background(), repo, "void", GrepOptions{}) 20 + assert.NoError(t, err) 21 + assert.Equal(t, []*GrepResult{ 22 + { 23 + Filename: "java-hello/main.java", 24 + LineNumbers: []int{3}, 25 + LineCodes: []string{" public static void main(String[] args)"}, 26 + }, 27 + { 28 + Filename: "main.vendor.java", 29 + LineNumbers: []int{3}, 30 + LineCodes: []string{" public static void main(String[] args)"}, 31 + }, 32 + }, res) 33 + 34 + res, err = GrepSearch(context.Background(), repo, "void", GrepOptions{MaxResultLimit: 1}) 35 + assert.NoError(t, err) 36 + assert.Equal(t, []*GrepResult{ 37 + { 38 + Filename: "java-hello/main.java", 39 + LineNumbers: []int{3}, 40 + LineCodes: []string{" public static void main(String[] args)"}, 41 + }, 42 + }, res) 43 + 44 + res, err = GrepSearch(context.Background(), repo, "no-such-content", GrepOptions{}) 45 + assert.NoError(t, err) 46 + assert.Len(t, res, 0) 47 + 48 + res, err = GrepSearch(context.Background(), &Repository{Path: "no-such-git-repo"}, "no-such-content", GrepOptions{}) 49 + assert.Error(t, err) 50 + assert.Len(t, res, 0) 51 + }
+18 -17
modules/indexer/code/search.go
··· 70 70 return nil 71 71 } 72 72 73 + func HighlightSearchResultCode(filename string, lineNums []int, code string) []ResultLine { 74 + // we should highlight the whole code block first, otherwise it doesn't work well with multiple line highlighting 75 + hl, _ := highlight.Code(filename, "", code) 76 + highlightedLines := strings.Split(string(hl), "\n") 77 + 78 + // The lineNums outputted by highlight.Code might not match the original lineNums, because "highlight" removes the last `\n` 79 + lines := make([]ResultLine, min(len(highlightedLines), len(lineNums))) 80 + for i := 0; i < len(lines); i++ { 81 + lines[i].Num = lineNums[i] 82 + lines[i].FormattedContent = template.HTML(highlightedLines[i]) 83 + } 84 + return lines 85 + } 86 + 73 87 func searchResult(result *internal.SearchResult, startIndex, endIndex int) (*Result, error) { 74 88 startLineNum := 1 + strings.Count(result.Content[:startIndex], "\n") 75 89 76 90 var formattedLinesBuffer bytes.Buffer 77 91 78 92 contentLines := strings.SplitAfter(result.Content[startIndex:endIndex], "\n") 79 - lines := make([]ResultLine, 0, len(contentLines)) 93 + lineNums := make([]int, 0, len(contentLines)) 80 94 index := startIndex 81 95 for i, line := range contentLines { 82 96 var err error ··· 91 105 line[closeActiveIndex:], 92 106 ) 93 107 } else { 94 - err = writeStrings(&formattedLinesBuffer, 95 - line, 96 - ) 108 + err = writeStrings(&formattedLinesBuffer, line) 97 109 } 98 110 if err != nil { 99 111 return nil, err 100 112 } 101 113 102 - lines = append(lines, ResultLine{Num: startLineNum + i}) 114 + lineNums = append(lineNums, startLineNum+i) 103 115 index += len(line) 104 116 } 105 117 106 - // we should highlight the whole code block first, otherwise it doesn't work well with multiple line highlighting 107 - hl, _ := highlight.Code(result.Filename, "", formattedLinesBuffer.String()) 108 - highlightedLines := strings.Split(string(hl), "\n") 109 - 110 - // The lines outputted by highlight.Code might not match the original lines, because "highlight" removes the last `\n` 111 - lines = lines[:min(len(highlightedLines), len(lines))] 112 - highlightedLines = highlightedLines[:len(lines)] 113 - for i := 0; i < len(lines); i++ { 114 - lines[i].FormattedContent = template.HTML(highlightedLines[i]) 115 - } 116 - 117 118 return &Result{ 118 119 RepoID: result.RepoID, 119 120 Filename: result.Filename, ··· 121 122 UpdatedUnix: result.UpdatedUnix, 122 123 Language: result.Language, 123 124 Color: result.Color, 124 - Lines: lines, 125 + Lines: HighlightSearchResultCode(result.Filename, lineNums, formattedLinesBuffer.String()), 125 126 }, nil 126 127 } 127 128
+9
modules/util/util.go
··· 212 212 func ToPointer[T any](val T) *T { 213 213 return &val 214 214 } 215 + 216 + // IfZero returns "def" if "v" is a zero value, otherwise "v" 217 + func IfZero[T comparable](v, def T) T { 218 + var zero T 219 + if v == zero { 220 + return def 221 + } 222 + return v 223 + }
+1
options/locale/locale_en-US.ini
··· 171 171 team_kind = Search teams... 172 172 code_kind = Search code... 173 173 code_search_unavailable = Code search is currently not available. Please contact the site administrator. 174 + code_search_by_git_grep = Current code search results are provided by "git grep". There might be better results if site administrator enables Repository Indexer. 174 175 package_kind = Search packages... 175 176 project_kind = Search projects... 176 177 branch_kind = Search branches...
+34 -22
routers/web/repo/search.go
··· 5 5 6 6 import ( 7 7 "net/http" 8 + "strings" 8 9 9 10 "code.gitea.io/gitea/models/db" 10 11 "code.gitea.io/gitea/modules/base" 12 + "code.gitea.io/gitea/modules/git" 11 13 code_indexer "code.gitea.io/gitea/modules/indexer/code" 12 14 "code.gitea.io/gitea/modules/setting" 13 15 "code.gitea.io/gitea/services/context" 14 - "code.gitea.io/gitea/services/repository/files" 15 16 ) 16 17 17 18 const tplSearch base.TplName = "repo/search" ··· 33 34 return 34 35 } 35 36 36 - ctx.Data["Repo"] = ctx.Repo.Repository 37 - 38 37 page := ctx.FormInt("page") 39 38 if page <= 0 { 40 39 page = 1 41 40 } 42 41 42 + var total int 43 + var searchResults []*code_indexer.Result 44 + var searchResultLanguages []*code_indexer.SearchResultLanguages 43 45 if setting.Indexer.RepoIndexerEnabled { 44 - ctx.Data["CodeIndexerDisabled"] = false 45 - 46 - total, searchResults, searchResultLanguages, err := code_indexer.PerformSearch(ctx, &code_indexer.SearchOptions{ 46 + var err error 47 + total, searchResults, searchResultLanguages, err = code_indexer.PerformSearch(ctx, &code_indexer.SearchOptions{ 47 48 RepoIDs: []int64{ctx.Repo.Repository.ID}, 48 49 Keyword: keyword, 49 50 IsKeywordFuzzy: isFuzzy, ··· 62 63 } else { 63 64 ctx.Data["CodeIndexerUnavailable"] = !code_indexer.IsAvailable(ctx) 64 65 } 65 - 66 - ctx.Data["SearchResults"] = searchResults 67 - ctx.Data["SearchResultLanguages"] = searchResultLanguages 68 - 69 - pager := context.NewPagination(total, setting.UI.RepoSearchPagingNum, page, 5) 70 - pager.SetDefaultParams(ctx) 71 - pager.AddParam(ctx, "l", "Language") 72 - ctx.Data["Page"] = pager 73 66 } else { 74 - data, err := files.NewRepoGrep(ctx, ctx.Repo.Repository, keyword) 67 + res, err := git.GrepSearch(ctx, ctx.Repo.GitRepo, keyword, git.GrepOptions{ContextLineNumber: 3, IsFuzzy: isFuzzy}) 75 68 if err != nil { 76 - ctx.ServerError("NewRepoGrep", err) 69 + ctx.ServerError("GrepSearch", err) 77 70 return 78 71 } 72 + total = len(res) 73 + pageStart := min((page-1)*setting.UI.RepoSearchPagingNum, len(res)) 74 + pageEnd := min(page*setting.UI.RepoSearchPagingNum, len(res)) 75 + res = res[pageStart:pageEnd] 76 + for _, r := range res { 77 + searchResults = append(searchResults, &code_indexer.Result{ 78 + RepoID: ctx.Repo.Repository.ID, 79 + Filename: r.Filename, 80 + CommitID: ctx.Repo.CommitID, 81 + // UpdatedUnix: not supported yet 82 + // Language: not supported yet 83 + // Color: not supported yet 84 + Lines: code_indexer.HighlightSearchResultCode(r.Filename, r.LineNumbers, strings.Join(r.LineCodes, "\n")), 85 + }) 86 + } 87 + } 79 88 80 - ctx.Data["CodeIndexerDisabled"] = true 81 - ctx.Data["SearchResults"] = data 89 + ctx.Data["CodeIndexerEnabled"] = setting.Indexer.RepoIndexerEnabled 90 + ctx.Data["Repo"] = ctx.Repo.Repository 91 + ctx.Data["SourcePath"] = ctx.Repo.Repository.Link() 92 + ctx.Data["SearchResults"] = searchResults 93 + ctx.Data["SearchResultLanguages"] = searchResultLanguages 82 94 83 - pager := context.NewPagination(len(data), setting.UI.RepoSearchPagingNum, page, 5) 84 - pager.SetDefaultParams(ctx) 85 - ctx.Data["Page"] = pager 86 - } 95 + pager := context.NewPagination(total, setting.UI.RepoSearchPagingNum, page, 5) 96 + pager.SetDefaultParams(ctx) 97 + pager.AddParam(ctx, "l", "Language") 98 + ctx.Data["Page"] = pager 87 99 88 100 ctx.HTML(http.StatusOK, tplSearch) 89 101 }
-111
services/repository/files/search.go
··· 1 - package files 2 - 3 - import ( 4 - "context" 5 - "html/template" 6 - "strconv" 7 - "strings" 8 - 9 - repo_model "code.gitea.io/gitea/models/repo" 10 - "code.gitea.io/gitea/modules/git" 11 - "code.gitea.io/gitea/modules/gitrepo" 12 - "code.gitea.io/gitea/modules/highlight" 13 - "code.gitea.io/gitea/modules/timeutil" 14 - 15 - "github.com/go-enry/go-enry/v2" 16 - ) 17 - 18 - type Result struct { 19 - RepoID int64 // ignored 20 - Filename string 21 - CommitID string // branch 22 - UpdatedUnix timeutil.TimeStamp // ignored 23 - Language string 24 - Color string 25 - Lines []ResultLine 26 - } 27 - 28 - type ResultLine struct { 29 - Num int64 30 - FormattedContent template.HTML 31 - } 32 - 33 - const pHEAD = "HEAD:" 34 - 35 - func NewRepoGrep(ctx context.Context, repo *repo_model.Repository, keyword string) ([]*Result, error) { 36 - t, _, err := gitrepo.RepositoryFromContextOrOpen(ctx, repo) 37 - if err != nil { 38 - return nil, err 39 - } 40 - 41 - data := []*Result{} 42 - 43 - stdout, _, err := git.NewCommand(ctx, 44 - "grep", 45 - "-1", // n before and after lines 46 - "-z", 47 - "--heading", 48 - "--break", // easier parsing 49 - "--fixed-strings", // disallow regex for now 50 - "-n", // line nums 51 - "-i", // ignore case 52 - "--full-name", // full file path, rel to repo 53 - //"--column", // for adding better highlighting support 54 - "-e", // for queries starting with "-" 55 - ). 56 - AddDynamicArguments(keyword). 57 - AddArguments("HEAD"). 58 - RunStdString(&git.RunOpts{Dir: t.Path}) 59 - if err != nil { 60 - return data, nil // non zero exit code when there are no results 61 - } 62 - 63 - for _, block := range strings.Split(stdout, "\n\n") { 64 - res := Result{CommitID: repo.DefaultBranch} 65 - 66 - linenum := []int64{} 67 - code := []string{} 68 - 69 - for _, line := range strings.Split(block, "\n") { 70 - if strings.HasPrefix(line, pHEAD) { 71 - res.Filename = strings.TrimPrefix(line, pHEAD) 72 - continue 73 - } 74 - 75 - if ln, after, ok := strings.Cut(line, "\x00"); ok { 76 - i, err := strconv.ParseInt(ln, 10, 64) 77 - if err != nil { 78 - continue 79 - } 80 - 81 - linenum = append(linenum, i) 82 - code = append(code, after) 83 - } 84 - } 85 - 86 - if res.Filename == "" || len(code) == 0 || len(linenum) == 0 { 87 - continue 88 - } 89 - 90 - var hl template.HTML 91 - 92 - hl, res.Language = highlight.Code(res.Filename, "", strings.Join(code, "\n")) 93 - res.Color = enry.GetColor(res.Language) 94 - 95 - hlCode := strings.Split(string(hl), "\n") 96 - n := min(len(hlCode), len(linenum)) 97 - 98 - res.Lines = make([]ResultLine, n) 99 - 100 - for i := 0; i < n; i++ { 101 - res.Lines[i] = ResultLine{ 102 - Num: linenum[i], 103 - FormattedContent: template.HTML(hlCode[i]), 104 - } 105 - } 106 - 107 - data = append(data, &res) 108 - } 109 - 110 - return data, nil 111 - }
-50
services/repository/files/search_test.go
··· 1 - package files 2 - 3 - import ( 4 - "testing" 5 - 6 - "code.gitea.io/gitea/models/unittest" 7 - "code.gitea.io/gitea/services/contexttest" 8 - 9 - "github.com/stretchr/testify/assert" 10 - ) 11 - 12 - func TestNewRepoGrep(t *testing.T) { 13 - unittest.PrepareTestEnv(t) 14 - ctx, _ := contexttest.MockContext(t, "user2/repo1") 15 - ctx.SetParams(":id", "1") 16 - contexttest.LoadRepo(t, ctx, 1) 17 - contexttest.LoadRepoCommit(t, ctx) 18 - contexttest.LoadUser(t, ctx, 2) 19 - contexttest.LoadGitRepo(t, ctx) 20 - defer ctx.Repo.GitRepo.Close() 21 - 22 - t.Run("with result", func(t *testing.T) { 23 - res, err := NewRepoGrep(ctx, ctx.Repo.Repository, "Description") 24 - assert.NoError(t, err) 25 - 26 - expected := []*Result{ 27 - { 28 - RepoID: 0, 29 - Filename: "README.md", 30 - CommitID: "master", 31 - UpdatedUnix: 0, 32 - Language: "Markdown", 33 - Color: "#083fa1", 34 - Lines: []ResultLine{ 35 - {Num: 2, FormattedContent: ""}, 36 - {Num: 3, FormattedContent: "Description for repo1"}, 37 - }, 38 - }, 39 - } 40 - 41 - assert.EqualValues(t, res, expected) 42 - }) 43 - 44 - t.Run("empty result", func(t *testing.T) { 45 - res, err := NewRepoGrep(ctx, ctx.Repo.Repository, "keyword that does not match in the repo") 46 - assert.NoError(t, err) 47 - 48 - assert.EqualValues(t, res, []*Result{}) 49 - }) 50 - }
+8 -17
templates/repo/home.tmpl
··· 5 5 {{template "base/alert" .}} 6 6 {{template "repo/code/recently_pushed_new_branches" .}} 7 7 {{if and (not .HideRepoInfo) (not .IsBlame)}} 8 - <div class="ui repo-description gt-word-break"> 9 - <div id="repo-desc" class="tw-text-16"> 8 + <div class="repo-description"> 9 + <div id="repo-desc" class="gt-word-break tw-text-16"> 10 10 {{$description := .Repository.DescriptionHTML $.Context}} 11 11 {{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}} 12 12 <a class="link" href="{{.Repository.Website}}">{{.Repository.Website}}</a> 13 13 </div> 14 - <div class="ui repo-search"> 15 - <form class="ui form ignore-dirty" action="{{.RepoLink}}/search" method="get"> 16 - <div class="field"> 17 - <div class="ui small action input{{if .CodeIndexerUnavailable}} disabled left icon{{end}}"{{if .CodeIndexerUnavailable}} data-tooltip-content="{{ctx.Locale.Tr "search.code_search_unavailable"}}"{{end}}> 18 - <input name="q" value="{{.Keyword}}"{{if .CodeIndexerUnavailable}} disabled{{end}} placeholder="{{ctx.Locale.Tr "search.code_kind"}}"> 19 - {{if .CodeIndexerUnavailable}} 20 - <i class="icon">{{svg "octicon-alert"}}</i> 21 - {{end}} 22 - <button class="ui small icon button"{{if .CodeIndexerUnavailable}} disabled{{end}} type="submit"> 23 - {{svg "octicon-search"}} 24 - </button> 25 - </div> 26 - </div> 27 - </form> 28 - </div> 14 + <form class="ignore-dirty" action="{{.RepoLink}}/search" method="get"> 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> 29 20 </div> 30 21 <div class="tw-flex tw-items-center tw-flex-wrap tw-gap-1" id="repo-topics"> 31 22 {{range .Topics}}<a class="ui repo-topic large label topic gt-m-0" href="{{AppSubUrl}}/explore/repos?q={{.Name}}&topic=1">{{.Name}}</a>{{end}}
+11 -4
templates/shared/search/code/search.tmpl
··· 11 11 <div class="ui error message"> 12 12 <p>{{ctx.Locale.Tr "search.code_search_unavailable"}}</p> 13 13 </div> 14 - {{else if .SearchResults}} 15 - {{template "shared/search/code/results" .}} 16 - {{else if .Keyword}} 17 - <div>{{ctx.Locale.Tr "search.no_results"}}</div> 14 + {{else}} 15 + {{if not .CodeIndexerEnabled}} 16 + <div class="ui message"> 17 + <p>{{ctx.Locale.Tr "search.code_search_by_git_grep"}}</p> 18 + </div> 19 + {{end}} 20 + {{if .SearchResults}} 21 + {{template "shared/search/code/results" .}} 22 + {{else if .Keyword}} 23 + <div>{{ctx.Locale.Tr "search.no_results"}}</div> 24 + {{end}} 18 25 {{end}} 19 26 </div>
+2
templates/shared/searchbottom.tmpl
··· 1 + {{if or .result.Language (not .result.UpdatedUnix.IsZero)}} 1 2 <div class="ui bottom attached table segment tw-flex tw-items-center tw-justify-between"> 2 3 <div class="tw-flex tw-items-center gt-ml-4"> 3 4 {{if .result.Language}} ··· 10 11 {{end}} 11 12 </div> 12 13 </div> 14 + {{end}}
+18 -49
tests/integration/repo_search_test.go
··· 11 11 repo_model "code.gitea.io/gitea/models/repo" 12 12 code_indexer "code.gitea.io/gitea/modules/indexer/code" 13 13 "code.gitea.io/gitea/modules/setting" 14 - "code.gitea.io/gitea/modules/test" 15 14 "code.gitea.io/gitea/tests" 16 15 17 16 "github.com/PuerkitoBio/goquery" 18 17 "github.com/stretchr/testify/assert" 19 18 ) 20 19 21 - func resultFilenames(t testing.TB, doc *goquery.Selection) []string { 22 - filenameSelections := doc.Find(".header").Find("span.file") 20 + func resultFilenames(t testing.TB, doc *HTMLDoc) []string { 21 + filenameSelections := doc.doc.Find(".repository.search").Find(".repo-search-result").Find(".header").Find("span.file") 23 22 result := make([]string, filenameSelections.Length()) 24 23 filenameSelections.Each(func(i int, selection *goquery.Selection) { 25 24 result[i] = selection.Text() ··· 27 26 return result 28 27 } 29 28 30 - func checkResultLinks(t *testing.T, substr string, doc *goquery.Selection) { 31 - t.Helper() 32 - linkSelections := doc.Find("a[href]") 33 - linkSelections.Each(func(i int, selection *goquery.Selection) { 34 - assert.Contains(t, selection.AttrOr("href", ""), substr) 35 - }) 36 - } 37 - 38 - func testSearchRepo(t *testing.T, useExternalIndexer bool) { 29 + func TestSearchRepo(t *testing.T) { 39 30 defer tests.PrepareTestEnv(t)() 40 - defer test.MockVariableValue(&setting.Indexer.RepoIndexerEnabled, useExternalIndexer)() 41 31 42 32 repo, err := repo_model.GetRepositoryByOwnerAndName(db.DefaultContext, "user2", "repo1") 43 33 assert.NoError(t, err) 44 34 45 - gitReference := "/branch/" + repo.DefaultBranch 35 + code_indexer.UpdateRepoIndexer(repo) 46 36 47 - if useExternalIndexer { 48 - gitReference = "/commit/" 49 - code_indexer.UpdateRepoIndexer(repo) 50 - } 37 + testSearch(t, "/user2/repo1/search?q=Description&page=1", []string{"README.md"}) 51 38 52 - testSearch(t, "/user2/repo1/search?q=Description&page=1", gitReference, []string{"README.md"}) 39 + setting.Indexer.IncludePatterns = setting.IndexerGlobFromString("**.txt") 40 + setting.Indexer.ExcludePatterns = setting.IndexerGlobFromString("**/y/**") 53 41 54 - if useExternalIndexer { 55 - setting.Indexer.IncludePatterns = setting.IndexerGlobFromString("**.txt") 56 - setting.Indexer.ExcludePatterns = setting.IndexerGlobFromString("**/y/**") 42 + repo, err = repo_model.GetRepositoryByOwnerAndName(db.DefaultContext, "user2", "glob") 43 + assert.NoError(t, err) 57 44 58 - repo, err = repo_model.GetRepositoryByOwnerAndName(db.DefaultContext, "user2", "glob") 59 - assert.NoError(t, err) 45 + code_indexer.UpdateRepoIndexer(repo) 60 46 61 - code_indexer.UpdateRepoIndexer(repo) 62 - 63 - testSearch(t, "/user2/glob/search?q=loren&page=1", gitReference, []string{"a.txt"}) 64 - testSearch(t, "/user2/glob/search?q=loren&page=1&t=match", gitReference, []string{"a.txt"}) 65 - testSearch(t, "/user2/glob/search?q=file3&page=1", gitReference, []string{"x/b.txt", "a.txt"}) 66 - testSearch(t, "/user2/glob/search?q=file3&page=1&t=match", gitReference, []string{"x/b.txt", "a.txt"}) 67 - testSearch(t, "/user2/glob/search?q=file4&page=1&t=match", gitReference, []string{"x/b.txt", "a.txt"}) 68 - testSearch(t, "/user2/glob/search?q=file5&page=1&t=match", gitReference, []string{"x/b.txt", "a.txt"}) 69 - } 47 + testSearch(t, "/user2/glob/search?q=loren&page=1", []string{"a.txt"}) 48 + testSearch(t, "/user2/glob/search?q=loren&page=1&t=match", []string{"a.txt"}) 49 + testSearch(t, "/user2/glob/search?q=file3&page=1", []string{"x/b.txt", "a.txt"}) 50 + testSearch(t, "/user2/glob/search?q=file3&page=1&t=match", []string{"x/b.txt", "a.txt"}) 51 + testSearch(t, "/user2/glob/search?q=file4&page=1&t=match", []string{"x/b.txt", "a.txt"}) 52 + testSearch(t, "/user2/glob/search?q=file5&page=1&t=match", []string{"x/b.txt", "a.txt"}) 70 53 } 71 54 72 - func TestIndexerSearchRepo(t *testing.T) { 73 - testSearchRepo(t, true) 74 - } 75 - 76 - func TestNoIndexerSearchRepo(t *testing.T) { 77 - testSearchRepo(t, false) 78 - } 79 - 80 - func testSearch(t *testing.T, url, gitRef string, expected []string) { 55 + func testSearch(t *testing.T, url string, expected []string) { 81 56 req := NewRequest(t, "GET", url) 82 57 resp := MakeRequest(t, req, http.StatusOK) 83 58 84 - doc := NewHTMLParser(t, resp.Body).doc. 85 - Find(".repository.search"). 86 - Find(".repo-search-result") 87 - 88 - filenames := resultFilenames(t, doc) 59 + filenames := resultFilenames(t, NewHTMLParser(t, resp.Body)) 89 60 assert.EqualValues(t, expected, filenames) 90 - 91 - checkResultLinks(t, gitRef, doc) 92 61 }