Skip to content

Commit c47e6ce

Browse files
author
Earl Warren
committed
Merge pull request '[FEAT] repo search using git grep' (#1594) from snematoda/forgejo:forgejo into forgejo
Reviewed-on: https://codeberg.org/forgejo/forgejo/pulls/1594 Reviewed-by: Gusted <[email protected]>
2 parents ec1b646 + 51fb6f3 commit c47e6ce

File tree

6 files changed

+245
-62
lines changed

6 files changed

+245
-62
lines changed

routers/web/repo/search.go

Lines changed: 35 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -10,17 +10,13 @@ import (
1010
"code.gitea.io/gitea/modules/context"
1111
code_indexer "code.gitea.io/gitea/modules/indexer/code"
1212
"code.gitea.io/gitea/modules/setting"
13+
"code.gitea.io/gitea/services/repository/files"
1314
)
1415

1516
const tplSearch base.TplName = "repo/search"
1617

1718
// Search render repository search page
1819
func Search(ctx *context.Context) {
19-
if !setting.Indexer.RepoIndexerEnabled {
20-
ctx.Redirect(ctx.Repo.RepoLink)
21-
return
22-
}
23-
2420
language := ctx.FormTrim("l")
2521
keyword := ctx.FormTrim("q")
2622

@@ -37,31 +33,49 @@ func Search(ctx *context.Context) {
3733
return
3834
}
3935

36+
ctx.Data["SourcePath"] = ctx.Repo.Repository.Link()
37+
4038
page := ctx.FormInt("page")
4139
if page <= 0 {
4240
page = 1
4341
}
4442

45-
total, searchResults, searchResultLanguages, err := code_indexer.PerformSearch(ctx, []int64{ctx.Repo.Repository.ID},
46-
language, keyword, page, setting.UI.RepoSearchPagingNum, isMatch)
47-
if err != nil {
48-
if code_indexer.IsAvailable(ctx) {
49-
ctx.ServerError("SearchResults", err)
50-
return
43+
if setting.Indexer.RepoIndexerEnabled {
44+
ctx.Data["CodeIndexerEnabled"] = true
45+
46+
total, searchResults, searchResultLanguages, err := code_indexer.PerformSearch(ctx, []int64{ctx.Repo.Repository.ID},
47+
language, keyword, page, setting.UI.RepoSearchPagingNum, isMatch)
48+
if err != nil {
49+
if code_indexer.IsAvailable(ctx) {
50+
ctx.ServerError("SearchResults", err)
51+
return
52+
}
53+
ctx.Data["CodeIndexerUnavailable"] = true
54+
} else {
55+
ctx.Data["CodeIndexerUnavailable"] = !code_indexer.IsAvailable(ctx)
5156
}
52-
ctx.Data["CodeIndexerUnavailable"] = true
57+
58+
ctx.Data["SearchResults"] = searchResults
59+
ctx.Data["SearchResultLanguages"] = searchResultLanguages
60+
61+
pager := context.NewPagination(total, setting.UI.RepoSearchPagingNum, page, 5)
62+
pager.SetDefaultParams(ctx)
63+
pager.AddParam(ctx, "l", "Language")
64+
ctx.Data["Page"] = pager
5365
} else {
54-
ctx.Data["CodeIndexerUnavailable"] = !code_indexer.IsAvailable(ctx)
55-
}
66+
data, err := files.NewRepoGrep(ctx, ctx.Repo.Repository, keyword)
67+
if err != nil {
68+
ctx.ServerError("NewRepoGrep", err)
69+
return
70+
}
5671

57-
ctx.Data["SourcePath"] = ctx.Repo.Repository.Link()
58-
ctx.Data["SearchResults"] = searchResults
59-
ctx.Data["SearchResultLanguages"] = searchResultLanguages
72+
ctx.Data["CodeIndexerEnabled"] = false
73+
ctx.Data["SearchResults"] = data
6074

61-
pager := context.NewPagination(total, setting.UI.RepoSearchPagingNum, page, 5)
62-
pager.SetDefaultParams(ctx)
63-
pager.AddParam(ctx, "l", "Language")
64-
ctx.Data["Page"] = pager
75+
pager := context.NewPagination(len(data), setting.UI.RepoSearchPagingNum, page, 5)
76+
pager.SetDefaultParams(ctx)
77+
ctx.Data["Page"] = pager
78+
}
6579

6680
ctx.HTML(http.StatusOK, tplSearch)
6781
}
Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,90 @@
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+
LineNumbers []int64
26+
FormattedLines template.HTML
27+
}
28+
29+
const pHEAD = "HEAD:"
30+
31+
func NewRepoGrep(ctx context.Context, repo *repo_model.Repository, keyword string) ([]*Result, error) {
32+
t, _, err := gitrepo.RepositoryFromContextOrOpen(ctx, repo)
33+
if err != nil {
34+
return nil, err
35+
}
36+
37+
data := []*Result{}
38+
39+
stdout, _, err := git.NewCommand(ctx,
40+
"grep",
41+
"-1", // n before and after lines
42+
"-z",
43+
"--heading",
44+
"--break", // easier parsing
45+
"--fixed-strings", // disallow regex for now
46+
"-n", // line nums
47+
"-i", // ignore case
48+
"--full-name", // full file path, rel to repo
49+
//"--column", // for adding better highlighting support
50+
).
51+
AddDynamicArguments(keyword).
52+
AddArguments("HEAD").
53+
RunStdString(&git.RunOpts{Dir: t.Path})
54+
if err != nil {
55+
return data, nil // non zero exit code when there are no results
56+
}
57+
58+
for _, block := range strings.Split(stdout, "\n\n") {
59+
res := Result{CommitID: repo.DefaultBranch}
60+
code := []string{}
61+
62+
for _, line := range strings.Split(block, "\n") {
63+
if strings.HasPrefix(line, pHEAD) {
64+
res.Filename = strings.TrimPrefix(line, pHEAD)
65+
continue
66+
}
67+
68+
if ln, after, ok := strings.Cut(line, "\x00"); ok {
69+
i, err := strconv.ParseInt(ln, 10, 64)
70+
if err != nil {
71+
continue
72+
}
73+
74+
res.LineNumbers = append(res.LineNumbers, i)
75+
code = append(code, after)
76+
}
77+
}
78+
79+
if res.Filename == "" || len(code) == 0 || len(res.LineNumbers) == 0 {
80+
continue
81+
}
82+
83+
res.FormattedLines, res.Language = highlight.Code(res.Filename, "", strings.Join(code, "\n"))
84+
res.Color = enry.GetColor(res.Language)
85+
86+
data = append(data, &res)
87+
}
88+
89+
return data, nil
90+
}
Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
package files
2+
3+
import (
4+
"testing"
5+
6+
"code.gitea.io/gitea/models/unittest"
7+
"code.gitea.io/gitea/modules/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+
LineNumbers: []int64{2, 3},
35+
FormattedLines: "\nDescription for repo1",
36+
},
37+
}
38+
39+
assert.EqualValues(t, res, expected)
40+
})
41+
42+
t.Run("empty result", func(t *testing.T) {
43+
res, err := NewRepoGrep(ctx, ctx.Repo.Repository, "keyword that does not match in the repo")
44+
assert.NoError(t, err)
45+
46+
assert.EqualValues(t, res, []*Result{})
47+
})
48+
}

templates/repo/home.tmpl

Lines changed: 14 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -11,23 +11,21 @@
1111
{{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}}
1212
<a class="link" href="{{.Repository.Website}}">{{.Repository.Website}}</a>
1313
</div>
14-
{{if .RepoSearchEnabled}}
15-
<div class="ui repo-search">
16-
<form class="ui form ignore-dirty" action="{{.RepoLink}}/search" method="get">
17-
<div class="field">
18-
<div class="ui small action input{{if .CodeIndexerUnavailable}} disabled left icon{{end}}"{{if .CodeIndexerUnavailable}} data-tooltip-content="{{ctx.Locale.Tr "repo.search.code_search_unavailable"}}"{{end}}>
19-
<input name="q" value="{{.Keyword}}"{{if .CodeIndexerUnavailable}} disabled{{end}} placeholder="{{ctx.Locale.Tr "repo.search.search_repo"}}">
20-
{{if .CodeIndexerUnavailable}}
21-
<i class="icon">{{svg "octicon-alert"}}</i>
22-
{{end}}
23-
<button class="ui small icon button"{{if .CodeIndexerUnavailable}} disabled{{end}} type="submit">
24-
{{svg "octicon-search"}}
25-
</button>
26-
</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 "repo.search.code_search_unavailable"}}"{{end}}>
18+
<input name="q" value="{{.Keyword}}"{{if .CodeIndexerUnavailable}} disabled{{end}} placeholder="{{ctx.Locale.Tr "repo.search.search_repo"}}">
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>
2725
</div>
28-
</form>
29-
</div>
30-
{{end}}
26+
</div>
27+
</form>
28+
</div>
3129
</div>
3230
<div class="gt-df gt-ac gt-fw gt-gap-2" id="repo-topics">
3331
{{range .Topics}}<a class="ui repo-topic large label topic gt-m-0" href="{{AppSubUrl}}/explore/repos?q={{.Name}}&topic=1">{{.Name}}</a>{{end}}

templates/repo/search.tmpl

Lines changed: 11 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -6,14 +6,16 @@
66
<form class="ui form ignore-dirty" method="get">
77
<div class="ui fluid action input">
88
<input name="q" value="{{.Keyword}}"{{if .CodeIndexerUnavailable}} disabled{{end}} placeholder="{{ctx.Locale.Tr "repo.search.search_repo"}}">
9-
<div class="ui dropdown selection {{if .CodeIndexerUnavailable}} disabled{{end}}" data-tooltip-content="{{ctx.Locale.Tr "repo.search.type.tooltip"}}">
10-
<input name="t" type="hidden"{{if .CodeIndexerUnavailable}} disabled{{end}} value="{{.queryType}}">{{svg "octicon-triangle-down" 14 "dropdown icon"}}
11-
<div class="text">{{ctx.Locale.Tr (printf "repo.search.%s" (or .queryType "fuzzy"))}}</div>
12-
<div class="menu">
13-
<div class="item" data-value="" data-tooltip-content="{{ctx.Locale.Tr "repo.search.fuzzy.tooltip"}}">{{ctx.Locale.Tr "repo.search.fuzzy"}}</div>
14-
<div class="item" data-value="match" data-tooltip-content="{{ctx.Locale.Tr "repo.search.match.tooltip"}}">{{ctx.Locale.Tr "repo.search.match"}}</div>
9+
{{if .CodeIndexerEnabled}}
10+
<div class="ui dropdown selection {{if .CodeIndexerUnavailable}} disabled{{end}}" data-tooltip-content="{{ctx.Locale.Tr "repo.search.type.tooltip"}}">
11+
<input name="t" type="hidden"{{if .CodeIndexerUnavailable}} disabled{{end}} value="{{.queryType}}">{{svg "octicon-triangle-down" 14 "dropdown icon"}}
12+
<div class="text">{{ctx.Locale.Tr (printf "repo.search.%s" (or .queryType "fuzzy"))}}</div>
13+
<div class="menu">
14+
<div class="item" data-value="" data-tooltip-content="{{ctx.Locale.Tr "repo.search.fuzzy.tooltip"}}">{{ctx.Locale.Tr "repo.search.fuzzy"}}</div>
15+
<div class="item" data-value="match" data-tooltip-content="{{ctx.Locale.Tr "repo.search.match.tooltip"}}">{{ctx.Locale.Tr "repo.search.match"}}</div>
16+
</div>
1517
</div>
16-
</div>
18+
{{end}}
1719
<button class="ui icon button"{{if .CodeIndexerUnavailable}} disabled{{end}} type="submit">{{svg "octicon-search" 16}}</button>
1820
</div>
1921
</form>
@@ -41,7 +43,7 @@
4143
<div class="diff-file-box diff-box file-content non-diff-file-content repo-search-result">
4244
<h4 class="ui top attached normal header gt-df gt-fw">
4345
<span class="file gt-f1">{{.Filename}}</span>
44-
<a role="button" class="ui basic tiny button" rel="nofollow" href="{{$.SourcePath}}/src/commit/{{PathEscape $result.CommitID}}/{{PathEscapeSegments .Filename}}">{{ctx.Locale.Tr "repo.diff.view_file"}}</a>
46+
<a role="button" class="ui basic tiny button" rel="nofollow" href="{{$.SourcePath}}/src/{{if $.CodeIndexerEnabled}}commit{{else}}branch{{end}}/{{PathEscape $result.CommitID}}/{{PathEscapeSegments .Filename}}">{{ctx.Locale.Tr "repo.diff.view_file"}}</a>
4547
</h4>
4648
<div class="ui attached table segment">
4749
<div class="file-body file-code code-view">
@@ -50,7 +52,7 @@
5052
<tr>
5153
<td class="lines-num">
5254
{{range .LineNumbers}}
53-
<a href="{{$.SourcePath}}/src/commit/{{PathEscape $result.CommitID}}/{{PathEscapeSegments $result.Filename}}#L{{.}}"><span>{{.}}</span></a>
55+
<a href="{{$.SourcePath}}/src/{{if $.CodeIndexerEnabled}}commit{{else}}branch{{end}}/{{PathEscape $result.CommitID}}/{{PathEscapeSegments $result.Filename}}#L{{.}}"><span>{{.}}</span></a>
5456
{{end}}
5557
</td>
5658
<td class="lines-code chroma"><code class="code-inner">{{.FormattedLines}}</code></td>

0 commit comments

Comments
 (0)