0
0
mirror of https://github.com/go-gitea/gitea.git synced 2025-12-09 00:21:31 +01:00
gitea/routers/api/v1/repo/code/search.go
2025-08-20 10:14:04 -07:00

198 lines
5.2 KiB
Go

// Copyright 2025 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package code
import (
"fmt"
"net/http"
"net/url"
"path"
"slices"
access_model "code.gitea.io/gitea/models/perm/access"
repo_model "code.gitea.io/gitea/models/repo"
"code.gitea.io/gitea/modules/container"
"code.gitea.io/gitea/modules/indexer"
"code.gitea.io/gitea/modules/indexer/code"
"code.gitea.io/gitea/modules/log"
"code.gitea.io/gitea/modules/setting"
"code.gitea.io/gitea/modules/structs"
"code.gitea.io/gitea/modules/util"
"code.gitea.io/gitea/routers/api/v1/utils"
"code.gitea.io/gitea/services/context"
"code.gitea.io/gitea/services/convert"
)
// GlobalSearch search codes in all accessible repositories with the given keyword.
func GlobalSearch(ctx *context.APIContext) {
// swagger:operation GET /search/code search GlobalSearch
// ---
// summary: Search for repositories
// produces:
// - application/json
// parameters:
// - name: q
// in: query
// description: keyword
// type: string
// - name: repo
// in: query
// description: multiple repository names to search in
// type: string
// collectionFormat: multi
// - name: mode
// in: query
// description: include search of keyword within repository description
// type: string
// enum: [exact, words, fuzzy, regexp]
// - name: language
// in: query
// description: filter by programming language
// type: integer
// format: int64
// - name: page
// in: query
// description: page number of results to return (1-based)
// type: integer
// - name: limit
// in: query
// description: page size of results
// type: integer
// responses:
// "200":
// "$ref": "#/responses/CodeSearchResults"
// "422":
// "$ref": "#/responses/validationError"
if !setting.Indexer.RepoIndexerEnabled {
ctx.APIError(http.StatusBadRequest, "Repository indexing is disabled")
return
}
q := ctx.FormTrim("q")
if q == "" {
ctx.APIError(http.StatusUnprocessableEntity, "Query cannot be empty")
return
}
var (
accessibleRepoIDs []int64
err error
isAdmin bool
)
if ctx.Doer != nil {
isAdmin = ctx.Doer.IsAdmin
}
// guest user or non-admin user
if ctx.Doer == nil || !isAdmin {
accessibleRepoIDs, err = repo_model.FindUserCodeAccessibleRepoIDs(ctx, ctx.Doer)
if err != nil {
ctx.APIErrorInternal(err)
return
}
}
repoNames := ctx.FormStrings("repo")
searchRepoIDs := make([]int64, 0, len(repoNames))
if len(repoNames) > 0 {
var err error
searchRepoIDs, err = repo_model.GetRepositoriesIDsByFullNames(ctx, repoNames)
if err != nil {
ctx.APIErrorInternal(err)
return
}
}
if len(searchRepoIDs) > 0 {
for i := 0; i < len(searchRepoIDs); i++ {
if !slices.Contains(accessibleRepoIDs, searchRepoIDs[i]) {
searchRepoIDs = append(searchRepoIDs[:i], searchRepoIDs[i+1:]...)
i--
}
}
}
if len(searchRepoIDs) > 0 {
accessibleRepoIDs = searchRepoIDs
}
searchMode := indexer.SearchModeType(ctx.FormString("mode"))
listOpts := utils.GetListOptions(ctx)
total, results, languages, err := code.PerformSearch(ctx, &code.SearchOptions{
Keyword: q,
RepoIDs: accessibleRepoIDs,
Language: ctx.FormString("language"),
SearchMode: searchMode,
Paginator: &listOpts,
NoHighlight: true, // Default to no highlighting for performance, we don't need to highlight in the API search results
})
if err != nil {
ctx.APIErrorInternal(err)
return
}
ctx.SetTotalCountHeader(int64(total))
searchResults := structs.CodeSearchResults{
TotalCount: int64(total),
}
for _, lang := range languages {
searchResults.Languages = append(searchResults.Languages, structs.CodeSearchResultLanguage{
Language: lang.Language,
Color: lang.Color,
Count: lang.Count,
})
}
repoIDs := make(container.Set[int64], len(results))
for _, result := range results {
repoIDs.Add(result.RepoID)
}
repos, err := repo_model.GetRepositoriesMapByIDs(ctx, repoIDs.Values())
if err != nil {
ctx.APIErrorInternal(err)
return
}
permissions := make(map[int64]access_model.Permission)
for _, repo := range repos {
permission, err := access_model.GetUserRepoPermission(ctx, repo, ctx.Doer)
if err != nil {
ctx.APIErrorInternal(err)
return
}
permissions[repo.ID] = permission
}
for _, result := range results {
repo, ok := repos[result.RepoID]
if !ok {
log.Error("Repository with ID %d not found for search result: %v", result.RepoID, result)
continue
}
apiURL := fmt.Sprintf("%s/contents/%s?ref=%s", repo.APIURL(), util.PathEscapeSegments(result.Filename), url.PathEscape(result.CommitID))
htmlURL := fmt.Sprintf("%s/blob/%s/%s", repo.HTMLURL(), url.PathEscape(result.CommitID), util.PathEscapeSegments(result.Filename))
ret := structs.CodeSearchResult{
Name: path.Base(result.Filename),
Path: result.Filename,
Sha: result.CommitID,
URL: apiURL,
HTMLURL: htmlURL,
Language: result.Language,
Repository: convert.ToRepo(ctx, repo, permissions[repo.ID]),
}
for _, line := range result.Lines {
ret.Lines = append(ret.Lines, structs.CodeSearchResultLine{
LineNumber: line.Num,
RawContent: line.RawContent,
})
}
searchResults.Items = append(searchResults.Items, ret)
}
ctx.JSON(200, searchResults)
}