[FEAT] add fallback repo search using git grep
This commit is contained in:
parent
0533022d63
commit
51fb6f3983
|
@ -10,17 +10,13 @@ import (
|
||||||
"code.gitea.io/gitea/modules/context"
|
"code.gitea.io/gitea/modules/context"
|
||||||
code_indexer "code.gitea.io/gitea/modules/indexer/code"
|
code_indexer "code.gitea.io/gitea/modules/indexer/code"
|
||||||
"code.gitea.io/gitea/modules/setting"
|
"code.gitea.io/gitea/modules/setting"
|
||||||
|
"code.gitea.io/gitea/services/repository/files"
|
||||||
)
|
)
|
||||||
|
|
||||||
const tplSearch base.TplName = "repo/search"
|
const tplSearch base.TplName = "repo/search"
|
||||||
|
|
||||||
// Search render repository search page
|
// Search render repository search page
|
||||||
func Search(ctx *context.Context) {
|
func Search(ctx *context.Context) {
|
||||||
if !setting.Indexer.RepoIndexerEnabled {
|
|
||||||
ctx.Redirect(ctx.Repo.RepoLink)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
language := ctx.FormTrim("l")
|
language := ctx.FormTrim("l")
|
||||||
keyword := ctx.FormTrim("q")
|
keyword := ctx.FormTrim("q")
|
||||||
|
|
||||||
|
@ -37,11 +33,16 @@ func Search(ctx *context.Context) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
ctx.Data["SourcePath"] = ctx.Repo.Repository.Link()
|
||||||
|
|
||||||
page := ctx.FormInt("page")
|
page := ctx.FormInt("page")
|
||||||
if page <= 0 {
|
if page <= 0 {
|
||||||
page = 1
|
page = 1
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if setting.Indexer.RepoIndexerEnabled {
|
||||||
|
ctx.Data["CodeIndexerEnabled"] = true
|
||||||
|
|
||||||
total, searchResults, searchResultLanguages, err := code_indexer.PerformSearch(ctx, []int64{ctx.Repo.Repository.ID},
|
total, searchResults, searchResultLanguages, err := code_indexer.PerformSearch(ctx, []int64{ctx.Repo.Repository.ID},
|
||||||
language, keyword, page, setting.UI.RepoSearchPagingNum, isMatch)
|
language, keyword, page, setting.UI.RepoSearchPagingNum, isMatch)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
@ -54,7 +55,6 @@ func Search(ctx *context.Context) {
|
||||||
ctx.Data["CodeIndexerUnavailable"] = !code_indexer.IsAvailable(ctx)
|
ctx.Data["CodeIndexerUnavailable"] = !code_indexer.IsAvailable(ctx)
|
||||||
}
|
}
|
||||||
|
|
||||||
ctx.Data["SourcePath"] = ctx.Repo.Repository.Link()
|
|
||||||
ctx.Data["SearchResults"] = searchResults
|
ctx.Data["SearchResults"] = searchResults
|
||||||
ctx.Data["SearchResultLanguages"] = searchResultLanguages
|
ctx.Data["SearchResultLanguages"] = searchResultLanguages
|
||||||
|
|
||||||
|
@ -62,6 +62,20 @@ func Search(ctx *context.Context) {
|
||||||
pager.SetDefaultParams(ctx)
|
pager.SetDefaultParams(ctx)
|
||||||
pager.AddParam(ctx, "l", "Language")
|
pager.AddParam(ctx, "l", "Language")
|
||||||
ctx.Data["Page"] = pager
|
ctx.Data["Page"] = pager
|
||||||
|
} else {
|
||||||
|
data, err := files.NewRepoGrep(ctx, ctx.Repo.Repository, keyword)
|
||||||
|
if err != nil {
|
||||||
|
ctx.ServerError("NewRepoGrep", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx.Data["CodeIndexerEnabled"] = false
|
||||||
|
ctx.Data["SearchResults"] = data
|
||||||
|
|
||||||
|
pager := context.NewPagination(len(data), setting.UI.RepoSearchPagingNum, page, 5)
|
||||||
|
pager.SetDefaultParams(ctx)
|
||||||
|
ctx.Data["Page"] = pager
|
||||||
|
}
|
||||||
|
|
||||||
ctx.HTML(http.StatusOK, tplSearch)
|
ctx.HTML(http.StatusOK, tplSearch)
|
||||||
}
|
}
|
||||||
|
|
90
services/repository/files/search.go
Normal file
90
services/repository/files/search.go
Normal file
|
@ -0,0 +1,90 @@
|
||||||
|
package files
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"html/template"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
repo_model "code.gitea.io/gitea/models/repo"
|
||||||
|
"code.gitea.io/gitea/modules/git"
|
||||||
|
"code.gitea.io/gitea/modules/gitrepo"
|
||||||
|
"code.gitea.io/gitea/modules/highlight"
|
||||||
|
"code.gitea.io/gitea/modules/timeutil"
|
||||||
|
|
||||||
|
"github.com/go-enry/go-enry/v2"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Result struct {
|
||||||
|
RepoID int64 // ignored
|
||||||
|
Filename string
|
||||||
|
CommitID string // branch
|
||||||
|
UpdatedUnix timeutil.TimeStamp // ignored
|
||||||
|
Language string
|
||||||
|
Color string
|
||||||
|
LineNumbers []int64
|
||||||
|
FormattedLines template.HTML
|
||||||
|
}
|
||||||
|
|
||||||
|
const pHEAD = "HEAD:"
|
||||||
|
|
||||||
|
func NewRepoGrep(ctx context.Context, repo *repo_model.Repository, keyword string) ([]*Result, error) {
|
||||||
|
t, _, err := gitrepo.RepositoryFromContextOrOpen(ctx, repo)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
data := []*Result{}
|
||||||
|
|
||||||
|
stdout, _, err := git.NewCommand(ctx,
|
||||||
|
"grep",
|
||||||
|
"-1", // n before and after lines
|
||||||
|
"-z",
|
||||||
|
"--heading",
|
||||||
|
"--break", // easier parsing
|
||||||
|
"--fixed-strings", // disallow regex for now
|
||||||
|
"-n", // line nums
|
||||||
|
"-i", // ignore case
|
||||||
|
"--full-name", // full file path, rel to repo
|
||||||
|
//"--column", // for adding better highlighting support
|
||||||
|
).
|
||||||
|
AddDynamicArguments(keyword).
|
||||||
|
AddArguments("HEAD").
|
||||||
|
RunStdString(&git.RunOpts{Dir: t.Path})
|
||||||
|
if err != nil {
|
||||||
|
return data, nil // non zero exit code when there are no results
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, block := range strings.Split(stdout, "\n\n") {
|
||||||
|
res := Result{CommitID: repo.DefaultBranch}
|
||||||
|
code := []string{}
|
||||||
|
|
||||||
|
for _, line := range strings.Split(block, "\n") {
|
||||||
|
if strings.HasPrefix(line, pHEAD) {
|
||||||
|
res.Filename = strings.TrimPrefix(line, pHEAD)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
if ln, after, ok := strings.Cut(line, "\x00"); ok {
|
||||||
|
i, err := strconv.ParseInt(ln, 10, 64)
|
||||||
|
if err != nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
res.LineNumbers = append(res.LineNumbers, i)
|
||||||
|
code = append(code, after)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if res.Filename == "" || len(code) == 0 || len(res.LineNumbers) == 0 {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
res.FormattedLines, res.Language = highlight.Code(res.Filename, "", strings.Join(code, "\n"))
|
||||||
|
res.Color = enry.GetColor(res.Language)
|
||||||
|
|
||||||
|
data = append(data, &res)
|
||||||
|
}
|
||||||
|
|
||||||
|
return data, nil
|
||||||
|
}
|
48
services/repository/files/search_test.go
Normal file
48
services/repository/files/search_test.go
Normal file
|
@ -0,0 +1,48 @@
|
||||||
|
package files
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"code.gitea.io/gitea/models/unittest"
|
||||||
|
"code.gitea.io/gitea/modules/contexttest"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestNewRepoGrep(t *testing.T) {
|
||||||
|
unittest.PrepareTestEnv(t)
|
||||||
|
ctx, _ := contexttest.MockContext(t, "user2/repo1")
|
||||||
|
ctx.SetParams(":id", "1")
|
||||||
|
contexttest.LoadRepo(t, ctx, 1)
|
||||||
|
contexttest.LoadRepoCommit(t, ctx)
|
||||||
|
contexttest.LoadUser(t, ctx, 2)
|
||||||
|
contexttest.LoadGitRepo(t, ctx)
|
||||||
|
defer ctx.Repo.GitRepo.Close()
|
||||||
|
|
||||||
|
t.Run("with result", func(t *testing.T) {
|
||||||
|
res, err := NewRepoGrep(ctx, ctx.Repo.Repository, "Description")
|
||||||
|
assert.NoError(t, err)
|
||||||
|
|
||||||
|
expected := []*Result{
|
||||||
|
{
|
||||||
|
RepoID: 0,
|
||||||
|
Filename: "README.md",
|
||||||
|
CommitID: "master",
|
||||||
|
UpdatedUnix: 0,
|
||||||
|
Language: "Markdown",
|
||||||
|
Color: "#083fa1",
|
||||||
|
LineNumbers: []int64{2, 3},
|
||||||
|
FormattedLines: "\nDescription for repo1",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
assert.EqualValues(t, res, expected)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("empty result", func(t *testing.T) {
|
||||||
|
res, err := NewRepoGrep(ctx, ctx.Repo.Repository, "keyword that does not match in the repo")
|
||||||
|
assert.NoError(t, err)
|
||||||
|
|
||||||
|
assert.EqualValues(t, res, []*Result{})
|
||||||
|
})
|
||||||
|
}
|
|
@ -11,7 +11,6 @@
|
||||||
{{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}}
|
{{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}}
|
||||||
<a class="link" href="{{.Repository.Website}}">{{.Repository.Website}}</a>
|
<a class="link" href="{{.Repository.Website}}">{{.Repository.Website}}</a>
|
||||||
</div>
|
</div>
|
||||||
{{if .RepoSearchEnabled}}
|
|
||||||
<div class="ui repo-search">
|
<div class="ui repo-search">
|
||||||
<form class="ui form ignore-dirty" action="{{.RepoLink}}/search" method="get">
|
<form class="ui form ignore-dirty" action="{{.RepoLink}}/search" method="get">
|
||||||
<div class="field">
|
<div class="field">
|
||||||
|
@ -27,7 +26,6 @@
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
{{end}}
|
|
||||||
</div>
|
</div>
|
||||||
<div class="gt-df gt-ac gt-fw gt-gap-2" id="repo-topics">
|
<div class="gt-df gt-ac gt-fw gt-gap-2" id="repo-topics">
|
||||||
{{range .Topics}}<a class="ui repo-topic large label topic gt-m-0" href="{{AppSubUrl}}/explore/repos?q={{.Name}}&topic=1">{{.Name}}</a>{{end}}
|
{{range .Topics}}<a class="ui repo-topic large label topic gt-m-0" href="{{AppSubUrl}}/explore/repos?q={{.Name}}&topic=1">{{.Name}}</a>{{end}}
|
||||||
|
|
|
@ -6,6 +6,7 @@
|
||||||
<form class="ui form ignore-dirty" method="get">
|
<form class="ui form ignore-dirty" method="get">
|
||||||
<div class="ui fluid action input">
|
<div class="ui fluid action input">
|
||||||
<input name="q" value="{{.Keyword}}"{{if .CodeIndexerUnavailable}} disabled{{end}} placeholder="{{ctx.Locale.Tr "repo.search.search_repo"}}">
|
<input name="q" value="{{.Keyword}}"{{if .CodeIndexerUnavailable}} disabled{{end}} placeholder="{{ctx.Locale.Tr "repo.search.search_repo"}}">
|
||||||
|
{{if .CodeIndexerEnabled}}
|
||||||
<div class="ui dropdown selection {{if .CodeIndexerUnavailable}} disabled{{end}}" data-tooltip-content="{{ctx.Locale.Tr "repo.search.type.tooltip"}}">
|
<div class="ui dropdown selection {{if .CodeIndexerUnavailable}} disabled{{end}}" data-tooltip-content="{{ctx.Locale.Tr "repo.search.type.tooltip"}}">
|
||||||
<input name="t" type="hidden"{{if .CodeIndexerUnavailable}} disabled{{end}} value="{{.queryType}}">{{svg "octicon-triangle-down" 14 "dropdown icon"}}
|
<input name="t" type="hidden"{{if .CodeIndexerUnavailable}} disabled{{end}} value="{{.queryType}}">{{svg "octicon-triangle-down" 14 "dropdown icon"}}
|
||||||
<div class="text">{{ctx.Locale.Tr (printf "repo.search.%s" (or .queryType "fuzzy"))}}</div>
|
<div class="text">{{ctx.Locale.Tr (printf "repo.search.%s" (or .queryType "fuzzy"))}}</div>
|
||||||
|
@ -14,6 +15,7 @@
|
||||||
<div class="item" data-value="match" data-tooltip-content="{{ctx.Locale.Tr "repo.search.match.tooltip"}}">{{ctx.Locale.Tr "repo.search.match"}}</div>
|
<div class="item" data-value="match" data-tooltip-content="{{ctx.Locale.Tr "repo.search.match.tooltip"}}">{{ctx.Locale.Tr "repo.search.match"}}</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
{{end}}
|
||||||
<button class="ui icon button"{{if .CodeIndexerUnavailable}} disabled{{end}} type="submit">{{svg "octicon-search" 16}}</button>
|
<button class="ui icon button"{{if .CodeIndexerUnavailable}} disabled{{end}} type="submit">{{svg "octicon-search" 16}}</button>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
|
@ -41,7 +43,7 @@
|
||||||
<div class="diff-file-box diff-box file-content non-diff-file-content repo-search-result">
|
<div class="diff-file-box diff-box file-content non-diff-file-content repo-search-result">
|
||||||
<h4 class="ui top attached normal header gt-df gt-fw">
|
<h4 class="ui top attached normal header gt-df gt-fw">
|
||||||
<span class="file gt-f1">{{.Filename}}</span>
|
<span class="file gt-f1">{{.Filename}}</span>
|
||||||
<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>
|
<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>
|
||||||
</h4>
|
</h4>
|
||||||
<div class="ui attached table segment">
|
<div class="ui attached table segment">
|
||||||
<div class="file-body file-code code-view">
|
<div class="file-body file-code code-view">
|
||||||
|
@ -50,7 +52,7 @@
|
||||||
<tr>
|
<tr>
|
||||||
<td class="lines-num">
|
<td class="lines-num">
|
||||||
{{range .LineNumbers}}
|
{{range .LineNumbers}}
|
||||||
<a href="{{$.SourcePath}}/src/commit/{{PathEscape $result.CommitID}}/{{PathEscapeSegments $result.Filename}}#L{{.}}"><span>{{.}}</span></a>
|
<a href="{{$.SourcePath}}/src/{{if $.CodeIndexerEnabled}}commit{{else}}branch{{end}}/{{PathEscape $result.CommitID}}/{{PathEscapeSegments $result.Filename}}#L{{.}}"><span>{{.}}</span></a>
|
||||||
{{end}}
|
{{end}}
|
||||||
</td>
|
</td>
|
||||||
<td class="lines-code chroma"><code class="code-inner">{{.FormattedLines}}</code></td>
|
<td class="lines-code chroma"><code class="code-inner">{{.FormattedLines}}</code></td>
|
||||||
|
|
|
@ -11,14 +11,15 @@ import (
|
||||||
repo_model "code.gitea.io/gitea/models/repo"
|
repo_model "code.gitea.io/gitea/models/repo"
|
||||||
code_indexer "code.gitea.io/gitea/modules/indexer/code"
|
code_indexer "code.gitea.io/gitea/modules/indexer/code"
|
||||||
"code.gitea.io/gitea/modules/setting"
|
"code.gitea.io/gitea/modules/setting"
|
||||||
|
"code.gitea.io/gitea/modules/test"
|
||||||
"code.gitea.io/gitea/tests"
|
"code.gitea.io/gitea/tests"
|
||||||
|
|
||||||
"github.com/PuerkitoBio/goquery"
|
"github.com/PuerkitoBio/goquery"
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
)
|
)
|
||||||
|
|
||||||
func resultFilenames(t testing.TB, doc *HTMLDoc) []string {
|
func resultFilenames(t testing.TB, doc *goquery.Selection) []string {
|
||||||
filenameSelections := doc.doc.Find(".repository.search").Find(".repo-search-result").Find(".header").Find("span.file")
|
filenameSelections := doc.Find(".header").Find("span.file")
|
||||||
result := make([]string, filenameSelections.Length())
|
result := make([]string, filenameSelections.Length())
|
||||||
filenameSelections.Each(func(i int, selection *goquery.Selection) {
|
filenameSelections.Each(func(i int, selection *goquery.Selection) {
|
||||||
result[i] = selection.Text()
|
result[i] = selection.Text()
|
||||||
|
@ -26,16 +27,31 @@ func resultFilenames(t testing.TB, doc *HTMLDoc) []string {
|
||||||
return result
|
return result
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestSearchRepo(t *testing.T) {
|
func checkResultLinks(t *testing.T, substr string, doc *goquery.Selection) {
|
||||||
|
t.Helper()
|
||||||
|
linkSelections := doc.Find("a[href]")
|
||||||
|
linkSelections.Each(func(i int, selection *goquery.Selection) {
|
||||||
|
assert.Contains(t, selection.AttrOr("href", ""), substr)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func testSearchRepo(t *testing.T, useExternalIndexer bool) {
|
||||||
defer tests.PrepareTestEnv(t)()
|
defer tests.PrepareTestEnv(t)()
|
||||||
|
defer test.MockVariableValue(&setting.Indexer.RepoIndexerEnabled, useExternalIndexer)()
|
||||||
|
|
||||||
repo, err := repo_model.GetRepositoryByOwnerAndName(db.DefaultContext, "user2", "repo1")
|
repo, err := repo_model.GetRepositoryByOwnerAndName(db.DefaultContext, "user2", "repo1")
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
|
|
||||||
|
gitReference := "/branch/" + repo.DefaultBranch
|
||||||
|
|
||||||
|
if useExternalIndexer {
|
||||||
|
gitReference = "/commit/"
|
||||||
executeIndexer(t, repo, code_indexer.UpdateRepoIndexer)
|
executeIndexer(t, repo, code_indexer.UpdateRepoIndexer)
|
||||||
|
}
|
||||||
|
|
||||||
testSearch(t, "/user2/repo1/search?q=Description&page=1", []string{"README.md"})
|
testSearch(t, "/user2/repo1/search?q=Description&page=1", gitReference, []string{"README.md"})
|
||||||
|
|
||||||
|
if useExternalIndexer {
|
||||||
setting.Indexer.IncludePatterns = setting.IndexerGlobFromString("**.txt")
|
setting.Indexer.IncludePatterns = setting.IndexerGlobFromString("**.txt")
|
||||||
setting.Indexer.ExcludePatterns = setting.IndexerGlobFromString("**/y/**")
|
setting.Indexer.ExcludePatterns = setting.IndexerGlobFromString("**/y/**")
|
||||||
|
|
||||||
|
@ -44,18 +60,33 @@ func TestSearchRepo(t *testing.T) {
|
||||||
|
|
||||||
executeIndexer(t, repo, code_indexer.UpdateRepoIndexer)
|
executeIndexer(t, repo, code_indexer.UpdateRepoIndexer)
|
||||||
|
|
||||||
testSearch(t, "/user2/glob/search?q=loren&page=1", []string{"a.txt"})
|
testSearch(t, "/user2/glob/search?q=loren&page=1", gitReference, []string{"a.txt"})
|
||||||
testSearch(t, "/user2/glob/search?q=file3&page=1", []string{"x/b.txt"})
|
testSearch(t, "/user2/glob/search?q=file3&page=1", gitReference, []string{"x/b.txt"})
|
||||||
testSearch(t, "/user2/glob/search?q=file4&page=1", []string{})
|
testSearch(t, "/user2/glob/search?q=file4&page=1", gitReference, []string{})
|
||||||
testSearch(t, "/user2/glob/search?q=file5&page=1", []string{})
|
testSearch(t, "/user2/glob/search?q=file5&page=1", gitReference, []string{})
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func testSearch(t *testing.T, url string, expected []string) {
|
func TestIndexerSearchRepo(t *testing.T) {
|
||||||
|
testSearchRepo(t, true)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestNoIndexerSearchRepo(t *testing.T) {
|
||||||
|
testSearchRepo(t, false)
|
||||||
|
}
|
||||||
|
|
||||||
|
func testSearch(t *testing.T, url, gitRef string, expected []string) {
|
||||||
req := NewRequest(t, "GET", url)
|
req := NewRequest(t, "GET", url)
|
||||||
resp := MakeRequest(t, req, http.StatusOK)
|
resp := MakeRequest(t, req, http.StatusOK)
|
||||||
|
|
||||||
filenames := resultFilenames(t, NewHTMLParser(t, resp.Body))
|
doc := NewHTMLParser(t, resp.Body).doc.
|
||||||
|
Find(".repository.search").
|
||||||
|
Find(".repo-search-result")
|
||||||
|
|
||||||
|
filenames := resultFilenames(t, doc)
|
||||||
assert.EqualValues(t, expected, filenames)
|
assert.EqualValues(t, expected, filenames)
|
||||||
|
|
||||||
|
checkResultLinks(t, gitRef, doc)
|
||||||
}
|
}
|
||||||
|
|
||||||
func executeIndexer(t *testing.T, repo *repo_model.Repository, op func(*repo_model.Repository)) {
|
func executeIndexer(t *testing.T, repo *repo_model.Repository, op func(*repo_model.Repository)) {
|
||||||
|
|
Loading…
Reference in a new issue