Merge pull request '[FEAT] Wiki Search' (#3847) from snematoda/wiki-search-grep into forgejo

Reviewed-on: https://codeberg.org/forgejo/forgejo/pulls/3847
Reviewed-by: Earl Warren <earl-warren@noreply.codeberg.org>
This commit is contained in:
Earl Warren 2024-05-21 06:27:30 +00:00
commit fb1338537b
10 changed files with 107 additions and 0 deletions

View file

@ -27,6 +27,7 @@ type GrepResult struct {
type GrepOptions struct { type GrepOptions struct {
RefName string RefName string
MaxResultLimit int MaxResultLimit int
MatchesPerFile int
ContextLineNumber int ContextLineNumber int
IsFuzzy bool IsFuzzy bool
PathSpec []setting.Glob PathSpec []setting.Glob
@ -54,6 +55,9 @@ func GrepSearch(ctx context.Context, repo *Repository, search string, opts GrepO
var results []*GrepResult var results []*GrepResult
cmd := NewCommand(ctx, "grep", "--null", "--break", "--heading", "--fixed-strings", "--line-number", "--ignore-case", "--full-name") cmd := NewCommand(ctx, "grep", "--null", "--break", "--heading", "--fixed-strings", "--line-number", "--ignore-case", "--full-name")
cmd.AddOptionValues("--context", fmt.Sprint(opts.ContextLineNumber)) cmd.AddOptionValues("--context", fmt.Sprint(opts.ContextLineNumber))
if opts.MatchesPerFile > 0 {
cmd.AddOptionValues("--max-count", fmt.Sprint(opts.MatchesPerFile))
}
if opts.IsFuzzy { if opts.IsFuzzy {
words := strings.Fields(search) words := strings.Fields(search)
for _, word := range words { for _, word := range words {

View file

@ -44,6 +44,31 @@ func TestGrepSearch(t *testing.T) {
}, },
}, res) }, res)
res, err = GrepSearch(context.Background(), repo, "world", GrepOptions{MatchesPerFile: 1})
assert.NoError(t, err)
assert.Equal(t, []*GrepResult{
{
Filename: "i-am-a-python.p",
LineNumbers: []int{1},
LineCodes: []string{"## This is a simple file to do a hello world"},
},
{
Filename: "java-hello/main.java",
LineNumbers: []int{1},
LineCodes: []string{"public class HelloWorld"},
},
{
Filename: "main.vendor.java",
LineNumbers: []int{1},
LineCodes: []string{"public class HelloWorld"},
},
{
Filename: "python-hello/hello.py",
LineNumbers: []int{1},
LineCodes: []string{"## This is a simple file to do a hello world"},
},
}, res)
res, err = GrepSearch(context.Background(), repo, "no-such-content", GrepOptions{}) res, err = GrepSearch(context.Background(), repo, "no-such-content", GrepOptions{})
assert.NoError(t, err) assert.NoError(t, err)
assert.Len(t, res, 0) assert.Len(t, res, 0)

View file

@ -2016,6 +2016,8 @@ wiki.pages = Pages
wiki.last_updated = Last updated %s wiki.last_updated = Last updated %s
wiki.page_name_desc = Enter a name for this Wiki page. Some special names are: "Home", "_Sidebar" and "_Footer". wiki.page_name_desc = Enter a name for this Wiki page. Some special names are: "Home", "_Sidebar" and "_Footer".
wiki.original_git_entry_tooltip = View original Git file instead of using friendly link. wiki.original_git_entry_tooltip = View original Git file instead of using friendly link.
wiki.search = Search wiki
wiki.no_search_results = No results
activity = Activity activity = Activity
activity.navbar.pulse = Pulse activity.navbar.pulse = Pulse

View file

@ -0,0 +1,3 @@
Basic wiki content search using git-grep
- The search results include the first ten matched files
- Only the first three matches per file are displayed

View file

@ -40,6 +40,7 @@ const (
tplWikiRevision base.TplName = "repo/wiki/revision" tplWikiRevision base.TplName = "repo/wiki/revision"
tplWikiNew base.TplName = "repo/wiki/new" tplWikiNew base.TplName = "repo/wiki/new"
tplWikiPages base.TplName = "repo/wiki/pages" tplWikiPages base.TplName = "repo/wiki/pages"
tplWikiSearch base.TplName = "repo/wiki/search"
) )
// MustEnableWiki check if wiki is enabled, if external then redirect // MustEnableWiki check if wiki is enabled, if external then redirect
@ -795,3 +796,20 @@ func DeleteWikiPagePost(ctx *context.Context) {
ctx.JSONRedirect(ctx.Repo.RepoLink + "/wiki/") ctx.JSONRedirect(ctx.Repo.RepoLink + "/wiki/")
} }
func WikiSearchContent(ctx *context.Context) {
keyword := ctx.FormTrim("q")
if keyword == "" {
ctx.HTML(http.StatusOK, tplWikiSearch)
return
}
res, err := wiki_service.SearchWikiContents(ctx, ctx.Repo.Repository, keyword)
if err != nil {
ctx.ServerError("SearchWikiContents", err)
return
}
ctx.Data["Results"] = res
ctx.HTML(http.StatusOK, tplWikiSearch)
}

View file

@ -1417,6 +1417,7 @@ func registerRoutes(m *web.Route) {
}) })
m.Group("/wiki", func() { m.Group("/wiki", func() {
m.Get("/search", repo.WikiSearchContent)
m.Get("/raw/*", repo.WikiRaw) m.Get("/raw/*", repo.WikiRaw)
}, repo.MustEnableWiki) }, repo.MustEnableWiki)

View file

@ -407,3 +407,19 @@ func DeleteWiki(ctx context.Context, repo *repo_model.Repository) error {
system_model.RemoveAllWithNotice(ctx, "Delete repository wiki", repo.WikiPath()) system_model.RemoveAllWithNotice(ctx, "Delete repository wiki", repo.WikiPath())
return nil return nil
} }
func SearchWikiContents(ctx context.Context, repo *repo_model.Repository, keyword string) ([]*git.GrepResult, error) {
gitRepo, err := git.OpenRepository(ctx, repo.WikiPath())
if err != nil {
return nil, err
}
defer gitRepo.Close()
return git.GrepSearch(ctx, gitRepo, keyword, git.GrepOptions{
ContextLineNumber: 0,
IsFuzzy: true,
RefName: repo.GetWikiBranchName(),
MaxResultLimit: 10,
MatchesPerFile: 3,
})
}

View file

@ -0,0 +1,12 @@
{{if .Results}}
{{range .Results}}
<a class="item" href="{{$.RepoLink}}/wiki/{{.Filename}}">
<b class="tw-block tw-mb-2">{{.Filename}}</b>
{{range .LineCodes}}
<p class="tw-my-0">{{.}}</p>
{{end}}
</a>
{{end}}
{{else}}
<div class="item muted">{{ctx.Locale.Tr "repo.wiki.no_search_results"}}</div>
{{end}}

View file

@ -32,6 +32,15 @@
{{template "repo/clone_buttons" .}} {{template "repo/clone_buttons" .}}
{{template "repo/clone_script" .}} {{template "repo/clone_script" .}}
</div> </div>
<div class="ui floating dropdown jump">
<div class="ui icon search input">
<i class="icon">{{svg "octicon-search"}}</i>
<input type="search" name="q" hx-get="{{$.RepoLink}}/wiki/search" hx-target="#wiki-search" hx-swap="innerHTML" hx-trigger="keyup changed delay:.5s" placeholder="{{ctx.Locale.Tr "repo.wiki.search"}}..." />
</div>
<div id="wiki-search" class="menu tw-absolute tw-mt-3 tw-rounded right">
<div class="item muted">{{ctx.Locale.Tr "repo.wiki.no_search_results"}}</div>
</div>
</div>
</div> </div>
<div class="ui dividing header"> <div class="ui dividing header">
<div class="ui stackable grid"> <div class="ui stackable grid">

View file

@ -15,9 +15,26 @@ import (
api "code.gitea.io/gitea/modules/structs" api "code.gitea.io/gitea/modules/structs"
"code.gitea.io/gitea/tests" "code.gitea.io/gitea/tests"
"github.com/PuerkitoBio/goquery"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
) )
func TestWikiSearchContent(t *testing.T) {
defer tests.PrepareTestEnv(t)()
req := NewRequest(t, "GET", "/user2/repo1/wiki/search?q=This")
resp := MakeRequest(t, req, http.StatusOK)
doc := NewHTMLParser(t, resp.Body)
res := doc.Find(".item > b").Map(func(_ int, el *goquery.Selection) string {
return el.Text()
})
assert.Equal(t, []string{
"Home.md",
"Page-With-Spaced-Name.md",
"Unescaped File.md",
}, res)
}
func TestWikiBranchNormalize(t *testing.T) { func TestWikiBranchNormalize(t *testing.T) {
defer tests.PrepareTestEnv(t)() defer tests.PrepareTestEnv(t)()