diff --git a/routers/web/repo/search.go b/routers/web/repo/search.go index 3c0fa4bc00..29b3b7b476 100644 --- a/routers/web/repo/search.go +++ b/routers/web/repo/search.go @@ -10,17 +10,13 @@ import ( "code.gitea.io/gitea/modules/context" code_indexer "code.gitea.io/gitea/modules/indexer/code" "code.gitea.io/gitea/modules/setting" + "code.gitea.io/gitea/services/repository/files" ) const tplSearch base.TplName = "repo/search" // Search render repository search page func Search(ctx *context.Context) { - if !setting.Indexer.RepoIndexerEnabled { - ctx.Redirect(ctx.Repo.RepoLink) - return - } - language := ctx.FormTrim("l") keyword := ctx.FormTrim("q") @@ -37,31 +33,49 @@ func Search(ctx *context.Context) { return } + ctx.Data["SourcePath"] = ctx.Repo.Repository.Link() + page := ctx.FormInt("page") if page <= 0 { page = 1 } - total, searchResults, searchResultLanguages, err := code_indexer.PerformSearch(ctx, []int64{ctx.Repo.Repository.ID}, - language, keyword, page, setting.UI.RepoSearchPagingNum, isMatch) - if err != nil { - if code_indexer.IsAvailable(ctx) { - ctx.ServerError("SearchResults", err) + if setting.Indexer.RepoIndexerEnabled { + ctx.Data["CodeIndexerEnabled"] = true + + total, searchResults, searchResultLanguages, err := code_indexer.PerformSearch(ctx, []int64{ctx.Repo.Repository.ID}, + language, keyword, page, setting.UI.RepoSearchPagingNum, isMatch) + if err != nil { + if code_indexer.IsAvailable(ctx) { + ctx.ServerError("SearchResults", err) + return + } + ctx.Data["CodeIndexerUnavailable"] = true + } else { + ctx.Data["CodeIndexerUnavailable"] = !code_indexer.IsAvailable(ctx) + } + + ctx.Data["SearchResults"] = searchResults + ctx.Data["SearchResultLanguages"] = searchResultLanguages + + pager := context.NewPagination(total, setting.UI.RepoSearchPagingNum, page, 5) + pager.SetDefaultParams(ctx) + pager.AddParam(ctx, "l", "Language") + ctx.Data["Page"] = pager + } else { + data, err := files.NewRepoGrep(ctx, ctx.Repo.Repository, keyword) + if err != nil { + ctx.ServerError("NewRepoGrep", err) return } - ctx.Data["CodeIndexerUnavailable"] = true - } else { - ctx.Data["CodeIndexerUnavailable"] = !code_indexer.IsAvailable(ctx) + + 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.Data["SourcePath"] = ctx.Repo.Repository.Link() - ctx.Data["SearchResults"] = searchResults - ctx.Data["SearchResultLanguages"] = searchResultLanguages - - pager := context.NewPagination(total, setting.UI.RepoSearchPagingNum, page, 5) - pager.SetDefaultParams(ctx) - pager.AddParam(ctx, "l", "Language") - ctx.Data["Page"] = pager - ctx.HTML(http.StatusOK, tplSearch) } diff --git a/services/repository/files/search.go b/services/repository/files/search.go new file mode 100644 index 0000000000..f8317c4892 --- /dev/null +++ b/services/repository/files/search.go @@ -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 +} diff --git a/services/repository/files/search_test.go b/services/repository/files/search_test.go new file mode 100644 index 0000000000..c24bb731a8 --- /dev/null +++ b/services/repository/files/search_test.go @@ -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{}) + }) +} diff --git a/templates/repo/home.tmpl b/templates/repo/home.tmpl index 5e27d9160c..9bac26ce1e 100644 --- a/templates/repo/home.tmpl +++ b/templates/repo/home.tmpl @@ -11,23 +11,21 @@ {{if $description}}{{$description | RenderCodeBlock}}{{else if .IsRepositoryAdmin}}{{ctx.Locale.Tr "repo.no_desc"}}{{end}} {{.Repository.Website}} - {{if .RepoSearchEnabled}} -
{{.FormattedLines}}