Skip to content

Add API route for explore/code search #31515

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 4 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions modules/indexer/code/internal/indexer.go
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ type SearchOptions struct {
Language string

IsKeywordFuzzy bool
IsHTMLSafe bool

db.Paginator
}
Expand Down
40 changes: 33 additions & 7 deletions modules/indexer/code/search.go
Original file line number Diff line number Diff line change
Expand Up @@ -28,18 +28,19 @@ type Result struct {
type ResultLine struct {
Num int
FormattedContent template.HTML
RawContent string
}

type SearchResultLanguages = internal.SearchResultLanguages

type SearchOptions = internal.SearchOptions

func indices(content string, selectionStartIndex, selectionEndIndex int) (int, int) {
func indices(content string, selectionStartIndex, selectionEndIndex, numLinesBuffer int) (int, int) {
startIndex := selectionStartIndex
numLinesBefore := 0
for ; startIndex > 0; startIndex-- {
if content[startIndex-1] == '\n' {
if numLinesBefore == 1 {
if numLinesBefore == numLinesBuffer {
break
}
numLinesBefore++
Expand All @@ -50,7 +51,7 @@ func indices(content string, selectionStartIndex, selectionEndIndex int) (int, i
numLinesAfter := 0
for ; endIndex < len(content); endIndex++ {
if content[endIndex] == '\n' {
if numLinesAfter == 1 {
if numLinesAfter == numLinesBuffer {
break
}
numLinesAfter++
Expand Down Expand Up @@ -86,7 +87,20 @@ func HighlightSearchResultCode(filename, language string, lineNums []int, code s
return lines
}

func searchResult(result *internal.SearchResult, startIndex, endIndex int) (*Result, error) {
func rawSearchResultCode(lineNums []int, code string) []*ResultLine {
rawLines := strings.Split(code, "\n")

lines := make([]*ResultLine, min(len(rawLines), len(lineNums)))
for i := 0; i < len(lines); i++ {
lines[i] = &ResultLine{
Num: lineNums[i],
RawContent: rawLines[i],
}
}
return lines
}

func searchResult(result *internal.SearchResult, startIndex, endIndex int, escapeHTML bool) (*Result, error) {
startLineNum := 1 + strings.Count(result.Content[:startIndex], "\n")

var formattedLinesBuffer bytes.Buffer
Expand Down Expand Up @@ -117,14 +131,21 @@ func searchResult(result *internal.SearchResult, startIndex, endIndex int) (*Res
index += len(line)
}

var lines []*ResultLine
if escapeHTML {
lines = HighlightSearchResultCode(result.Filename, result.Language, lineNums, formattedLinesBuffer.String())
} else {
lines = rawSearchResultCode(lineNums, formattedLinesBuffer.String())
}

return &Result{
RepoID: result.RepoID,
Filename: result.Filename,
CommitID: result.CommitID,
UpdatedUnix: result.UpdatedUnix,
Language: result.Language,
Color: result.Color,
Lines: HighlightSearchResultCode(result.Filename, result.Language, lineNums, formattedLinesBuffer.String()),
Lines: lines,
}, nil
}

Expand All @@ -142,9 +163,14 @@ func PerformSearch(ctx context.Context, opts *SearchOptions) (int, []*Result, []

displayResults := make([]*Result, len(results))

nLinesBuffer := 0
if opts.IsHTMLSafe {
nLinesBuffer = 1
}

for i, result := range results {
startIndex, endIndex := indices(result.Content, result.StartIndex, result.EndIndex)
displayResults[i], err = searchResult(result, startIndex, endIndex)
startIndex, endIndex := indices(result.Content, result.StartIndex, result.EndIndex, nLinesBuffer)
displayResults[i], err = searchResult(result, startIndex, endIndex, opts.IsHTMLSafe)
if err != nil {
return 0, nil, nil, err
}
Expand Down
20 changes: 20 additions & 0 deletions modules/structs/explore.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
// Copyright 2024 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT

package structs

// ExploreCodeSearchItem A code search match
// swagger:model
type ExploreCodeSearchItem struct {
RepoName string `json:"repoName"`
FilePath string `json:"path"`
LineNumber int `json:"lineNumber"`
LineText string `json:"lineText"`
}

// ExploreCodeResult all returned code search results
// swagger:model
type ExploreCodeResult struct {
Total int `json:"total"`
Results []ExploreCodeSearchItem `json:"results"`
}
9 changes: 9 additions & 0 deletions routers/api/v1/api.go
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,7 @@ import (
"code.gitea.io/gitea/modules/web"
"code.gitea.io/gitea/routers/api/v1/activitypub"
"code.gitea.io/gitea/routers/api/v1/admin"
"code.gitea.io/gitea/routers/api/v1/explore"
"code.gitea.io/gitea/routers/api/v1/misc"
"code.gitea.io/gitea/routers/api/v1/notify"
"code.gitea.io/gitea/routers/api/v1/org"
Expand Down Expand Up @@ -890,6 +891,14 @@ func Routes() *web.Router {
// Misc (public accessible)
m.Group("", func() {
m.Get("/version", misc.Version)
m.Group("/explore", func() {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Shouldn't the API route be /search/code to align with the naming conventions of the GitHub API?

Suggested change
m.Group("/explore", func() {
m.Group("/search", func() {

m.Get("/code", func(ctx *context.APIContext) {
if unit.TypeCode.UnitGlobalDisabled() {
ctx.NotFound("Repo unit code is disabled", nil)
return
}
}, explore.Code)
})
m.Get("/signing-key.gpg", misc.SigningKey)
m.Post("/markup", reqToken(), bind(api.MarkupOption{}), misc.Markup)
m.Post("/markdown", reqToken(), bind(api.MarkdownOption{}), misc.Markdown)
Expand Down
143 changes: 143 additions & 0 deletions routers/api/v1/explore/code.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,143 @@
// Copyright 2024 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT

package explore

import (
"net/http"
"slices"

"code.gitea.io/gitea/models/db"
repo_model "code.gitea.io/gitea/models/repo"
code_indexer "code.gitea.io/gitea/modules/indexer/code"
"code.gitea.io/gitea/modules/setting"
api "code.gitea.io/gitea/modules/structs"
"code.gitea.io/gitea/services/context"
"code.gitea.io/gitea/services/convert"
)

// Code explore code
func Code(ctx *context.APIContext) {
// swagger:operation GET /explore/code explore codeSearch
// ---
// summary: Search for code
// produces:
// - application/json
// parameters:
// - name: q
// in: query
// description: keyword
// type: string
// - name: page
// in: query
// description: page number of results to return (1-based)
// type: integer
// - name: fuzzy
// in: query
// description: whether to search fuzzy or strict (defaults to true)
// type: boolean
// responses:
// "200":
// description: "SearchResults of a successful search"
// schema:
// "$ref": "#/definitions/ExploreCodeResult"
if !setting.Indexer.RepoIndexerEnabled {
ctx.NotFound("Indexer not enabled")
return
}

keyword := ctx.FormTrim("q")

isFuzzy := ctx.FormOptionalBool("fuzzy").ValueOrDefault(true)

if keyword == "" {
ctx.JSON(http.StatusOK, api.ExploreCodeResult{
Total: 0,
Results: make([]api.ExploreCodeSearchItem, 0),
})
return
}

page := ctx.FormInt("page")
if page <= 0 {
page = 1
}

var (
repoIDs []int64
err error
isAdmin bool
)
if ctx.Doer != nil {
isAdmin = ctx.Doer.IsAdmin
}

if ctx.Doer == nil || !isAdmin {
repoIDs, err = repo_model.FindUserCodeAccessibleRepoIDs(ctx, ctx.Doer)
if err != nil {
ctx.JSON(http.StatusInternalServerError, api.SearchError{
OK: false,
Error: err.Error(),
})
return
}
}

var (
total int
searchResults []*code_indexer.Result
repoMaps map[int64]*repo_model.Repository
)

if (len(repoIDs) > 0) || isAdmin {
total, searchResults, _, err = code_indexer.PerformSearch(ctx, &code_indexer.SearchOptions{
RepoIDs: repoIDs,
Keyword: keyword,
IsKeywordFuzzy: isFuzzy,
IsHTMLSafe: false,
Paginator: &db.ListOptions{
Page: page,
PageSize: setting.API.DefaultPagingNum,
},
})
if err != nil {
if code_indexer.IsAvailable(ctx) {
ctx.JSON(http.StatusInternalServerError, api.SearchError{
OK: false,
Error: err.Error(),
})
return
}
}

loadRepoIDs := make([]int64, 0, len(searchResults))
for _, result := range searchResults {
if !slices.Contains(loadRepoIDs, result.RepoID) {
loadRepoIDs = append(loadRepoIDs, result.RepoID)
}
}

repoMaps, err = repo_model.GetRepositoriesMapByIDs(ctx, loadRepoIDs)
if err != nil {
ctx.JSON(http.StatusInternalServerError, api.SearchError{
OK: false,
Error: err.Error(),
})
return
}

if len(loadRepoIDs) != len(repoMaps) {
// Remove deleted repos from search results
cleanedSearchResults := make([]*code_indexer.Result, 0, len(repoMaps))
for _, sr := range searchResults {
if _, found := repoMaps[sr.RepoID]; found {
cleanedSearchResults = append(cleanedSearchResults, sr)
}
}

searchResults = cleanedSearchResults
}
}

ctx.JSON(http.StatusOK, convert.ToExploreCodeSearchResults(total, searchResults, repoMaps))
}
15 changes: 15 additions & 0 deletions routers/api/v1/swagger/explore.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
// Copyright 2024 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT

package swagger

import (
api "code.gitea.io/gitea/modules/structs"
)

// ExploreCode
// swagger:response ExploreCode
type swaggerResponseExploreCode struct {
// in:body
Body api.ExploreCodeResult `json:"body"`
}
1 change: 1 addition & 0 deletions routers/web/explore/code.go
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,7 @@ func Code(ctx *context.Context) {
RepoIDs: repoIDs,
Keyword: keyword,
IsKeywordFuzzy: isFuzzy,
IsHTMLSafe: true,
Language: language,
Paginator: &db.ListOptions{
Page: page,
Expand Down
1 change: 1 addition & 0 deletions routers/web/repo/search.go
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,7 @@ func Search(ctx *context.Context) {
RepoIDs: []int64{ctx.Repo.Repository.ID},
Keyword: keyword,
IsKeywordFuzzy: isFuzzy,
IsHTMLSafe: true,
Language: language,
Paginator: &db.ListOptions{
Page: page,
Expand Down
1 change: 1 addition & 0 deletions routers/web/user/code.go
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,7 @@ func CodeSearch(ctx *context.Context) {
Keyword: keyword,
IsKeywordFuzzy: isFuzzy,
Language: language,
IsHTMLSafe: true,
Paginator: &db.ListOptions{
Page: page,
PageSize: setting.UI.RepoSearchPagingNum,
Expand Down
30 changes: 30 additions & 0 deletions services/convert/explore.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
// Copyright 2024 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT

package convert

import (
repo_model "code.gitea.io/gitea/models/repo"
code_indexer "code.gitea.io/gitea/modules/indexer/code"
api "code.gitea.io/gitea/modules/structs"
)

func ToExploreCodeSearchResults(total int, results []*code_indexer.Result, repoMaps map[int64]*repo_model.Repository) api.ExploreCodeResult {
out := api.ExploreCodeResult{
Total: total,
Results: make([]api.ExploreCodeSearchItem, 0, len(results)),
}
for _, res := range results {
if repo := repoMaps[res.RepoID]; repo != nil {
for _, r := range res.Lines {
out.Results = append(out.Results, api.ExploreCodeSearchItem{
RepoName: repo.FullName(),
FilePath: res.Filename,
LineNumber: r.Num,
LineText: r.RawContent,
})
}
}
}
return out
}
Loading