add explore/code api route

This commit is contained in:
aaronknudtson 2024-06-27 22:11:59 -04:00
parent d655ff18b3
commit 1bf78712d4
9 changed files with 212 additions and 7 deletions

View File

@ -26,6 +26,7 @@ type SearchOptions struct {
Language string
IsKeywordFuzzy bool
IsHtmlSafe bool
db.Paginator
}

View File

@ -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++
@ -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++
@ -86,7 +87,22 @@ func HighlightSearchResultCode(filename, language string, lineNums []int, code s
return lines
}
func searchResult(result *internal.SearchResult, startIndex, endIndex int) (*Result, error) {
func RawSearchResultCode(filename, language string, lineNums []int, code string) []*ResultLine {
// we should highlight the whole code block first, otherwise it doesn't work well with multiple line highlighting
rawLines := strings.Split(code, "\n")
// The lineNums outputted by highlight.Code might not match the original lineNums, because "highlight" removes the last `\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
@ -117,6 +133,13 @@ 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(result.Filename, result.Language, lineNums, formattedLinesBuffer.String())
}
return &Result{
RepoID: result.RepoID,
Filename: result.Filename,
@ -124,7 +147,7 @@ func searchResult(result *internal.SearchResult, startIndex, endIndex int) (*Res
UpdatedUnix: result.UpdatedUnix,
Language: result.Language,
Color: result.Color,
Lines: HighlightSearchResultCode(result.Filename, result.Language, lineNums, formattedLinesBuffer.String()),
Lines: lines,
}, nil
}
@ -142,9 +165,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
}

View File

@ -0,0 +1,20 @@
// Copyright 2024 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package structs // import "code.gitea.io/gitea/modules/structs"
// ExploreCodeSearchItem A single 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 search results
// swagger:model
type ExploreCodeResult struct {
Total int `json:"total"`
Results []ExploreCodeSearchItem `json:"results"`
}

View File

@ -92,6 +92,7 @@ import (
"code.gitea.io/gitea/routers/api/v1/repo"
"code.gitea.io/gitea/routers/api/v1/settings"
"code.gitea.io/gitea/routers/api/v1/user"
"code.gitea.io/gitea/routers/api/v1/explore"
"code.gitea.io/gitea/routers/common"
"code.gitea.io/gitea/services/actions"
"code.gitea.io/gitea/services/auth"
@ -890,6 +891,14 @@ func Routes() *web.Router {
// Misc (public accessible)
m.Group("", func() {
m.Get("/version", misc.Version)
m.Group("/explore", 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)

View File

@ -0,0 +1,117 @@
// Copyright 2021 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package explore
import (
"net/http"
"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) {
if !setting.Indexer.RepoIndexerEnabled {
ctx.NotFound("Indexer not enabled")
return
}
language := ctx.FormTrim("l")
keyword := ctx.FormTrim("q")
isFuzzy := ctx.FormOptionalBool("fuzzy").ValueOrDefault(true)
if keyword == "" {
ctx.JSON(http.StatusInternalServerError, api.SearchError{OK: false, Error: "No keyword provided"})
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
}
// guest user or non-admin user
if ctx.Doer == nil || !isAdmin {
repoIDs, err = repo_model.FindUserCodeAccessibleRepoIDs(ctx, ctx.Doer)
if err != nil {
ctx.ServerError("FindUserCodeAccessibleRepoIDs", err)
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,
Language: language,
Paginator: &db.ListOptions{
Page: page,
PageSize: setting.API.DefaultPagingNum,
},
})
if err != nil {
if code_indexer.IsAvailable(ctx) {
ctx.ServerError("SearchResults", err)
return
}
}
loadRepoIDs := make([]int64, 0, len(searchResults))
for _, result := range searchResults {
var find bool
for _, id := range loadRepoIDs {
if id == result.RepoID {
find = true
break
}
}
if !find {
loadRepoIDs = append(loadRepoIDs, result.RepoID)
}
}
repoMaps, err = repo_model.GetRepositoriesMapByIDs(ctx, loadRepoIDs)
if err != nil {
ctx.ServerError("GetRepositoriesMapByIDs", err)
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))
}

View File

@ -81,6 +81,7 @@ func Code(ctx *context.Context) {
RepoIDs: repoIDs,
Keyword: keyword,
IsKeywordFuzzy: isFuzzy,
IsHtmlSafe: true,
Language: language,
Paginator: &db.ListOptions{
Page: page,

View File

@ -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,

View File

@ -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,

View File

@ -0,0 +1,27 @@
// 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}
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.Name,
FilePath: res.Filename,
LineNumber: r.Num,
LineText: r.RawContent,
})
}
}
}
return out
}