Skip to content

Commit 1bf7871

Browse files
committed
add explore/code api route
1 parent d655ff1 commit 1bf7871

File tree

9 files changed

+212
-7
lines changed

9 files changed

+212
-7
lines changed

modules/indexer/code/internal/indexer.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ type SearchOptions struct {
2626
Language string
2727

2828
IsKeywordFuzzy bool
29+
IsHtmlSafe bool
2930

3031
db.Paginator
3132
}

modules/indexer/code/search.go

Lines changed: 35 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -28,18 +28,19 @@ type Result struct {
2828
type ResultLine struct {
2929
Num int
3030
FormattedContent template.HTML
31+
RawContent string
3132
}
3233

3334
type SearchResultLanguages = internal.SearchResultLanguages
3435

3536
type SearchOptions = internal.SearchOptions
3637

37-
func indices(content string, selectionStartIndex, selectionEndIndex int) (int, int) {
38+
func indices(content string, selectionStartIndex, selectionEndIndex, numLinesBuffer int) (int, int) {
3839
startIndex := selectionStartIndex
3940
numLinesBefore := 0
4041
for ; startIndex > 0; startIndex-- {
4142
if content[startIndex-1] == '\n' {
42-
if numLinesBefore == 1 {
43+
if numLinesBefore == numLinesBuffer {
4344
break
4445
}
4546
numLinesBefore++
@@ -50,7 +51,7 @@ func indices(content string, selectionStartIndex, selectionEndIndex int) (int, i
5051
numLinesAfter := 0
5152
for ; endIndex < len(content); endIndex++ {
5253
if content[endIndex] == '\n' {
53-
if numLinesAfter == 1 {
54+
if numLinesAfter == numLinesBuffer {
5455
break
5556
}
5657
numLinesAfter++
@@ -86,7 +87,22 @@ func HighlightSearchResultCode(filename, language string, lineNums []int, code s
8687
return lines
8788
}
8889

89-
func searchResult(result *internal.SearchResult, startIndex, endIndex int) (*Result, error) {
90+
func RawSearchResultCode(filename, language string, lineNums []int, code string) []*ResultLine {
91+
// we should highlight the whole code block first, otherwise it doesn't work well with multiple line highlighting
92+
rawLines := strings.Split(code, "\n")
93+
94+
// The lineNums outputted by highlight.Code might not match the original lineNums, because "highlight" removes the last `\n`
95+
lines := make([]*ResultLine, min(len(rawLines), len(lineNums)))
96+
for i := 0; i < len(lines); i++ {
97+
lines[i] = &ResultLine{
98+
Num: lineNums[i],
99+
RawContent: rawLines[i],
100+
}
101+
}
102+
return lines
103+
}
104+
105+
func searchResult(result *internal.SearchResult, startIndex, endIndex int, escapeHtml bool) (*Result, error) {
90106
startLineNum := 1 + strings.Count(result.Content[:startIndex], "\n")
91107

92108
var formattedLinesBuffer bytes.Buffer
@@ -117,14 +133,21 @@ func searchResult(result *internal.SearchResult, startIndex, endIndex int) (*Res
117133
index += len(line)
118134
}
119135

136+
var lines []*ResultLine
137+
if escapeHtml {
138+
lines = HighlightSearchResultCode(result.Filename, result.Language, lineNums, formattedLinesBuffer.String())
139+
} else {
140+
lines = RawSearchResultCode(result.Filename, result.Language, lineNums, formattedLinesBuffer.String())
141+
}
142+
120143
return &Result{
121144
RepoID: result.RepoID,
122145
Filename: result.Filename,
123146
CommitID: result.CommitID,
124147
UpdatedUnix: result.UpdatedUnix,
125148
Language: result.Language,
126149
Color: result.Color,
127-
Lines: HighlightSearchResultCode(result.Filename, result.Language, lineNums, formattedLinesBuffer.String()),
150+
Lines: lines,
128151
}, nil
129152
}
130153

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

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

168+
nLinesBuffer := 0
169+
if opts.IsHtmlSafe {
170+
nLinesBuffer = 1
171+
}
172+
145173
for i, result := range results {
146-
startIndex, endIndex := indices(result.Content, result.StartIndex, result.EndIndex)
147-
displayResults[i], err = searchResult(result, startIndex, endIndex)
174+
startIndex, endIndex := indices(result.Content, result.StartIndex, result.EndIndex, nLinesBuffer)
175+
displayResults[i], err = searchResult(result, startIndex, endIndex, opts.IsHtmlSafe)
148176
if err != nil {
149177
return 0, nil, nil, err
150178
}

modules/structs/explore.go

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
// Copyright 2024 The Gitea Authors. All rights reserved.
2+
// SPDX-License-Identifier: MIT
3+
4+
package structs // import "code.gitea.io/gitea/modules/structs"
5+
6+
// ExploreCodeSearchItem A single search match
7+
// swagger:model
8+
type ExploreCodeSearchItem struct {
9+
RepoName string `json:"repoName"`
10+
FilePath string `json:"path"`
11+
LineNumber int `json:"lineNumber"`
12+
LineText string `json:"lineText"`
13+
}
14+
15+
// ExploreCodeResult all returned search results
16+
// swagger:model
17+
type ExploreCodeResult struct {
18+
Total int `json:"total"`
19+
Results []ExploreCodeSearchItem `json:"results"`
20+
}

routers/api/v1/api.go

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -92,6 +92,7 @@ import (
9292
"code.gitea.io/gitea/routers/api/v1/repo"
9393
"code.gitea.io/gitea/routers/api/v1/settings"
9494
"code.gitea.io/gitea/routers/api/v1/user"
95+
"code.gitea.io/gitea/routers/api/v1/explore"
9596
"code.gitea.io/gitea/routers/common"
9697
"code.gitea.io/gitea/services/actions"
9798
"code.gitea.io/gitea/services/auth"
@@ -890,6 +891,14 @@ func Routes() *web.Router {
890891
// Misc (public accessible)
891892
m.Group("", func() {
892893
m.Get("/version", misc.Version)
894+
m.Group("/explore", func() {
895+
m.Get("/code", func(ctx *context.APIContext) {
896+
if unit.TypeCode.UnitGlobalDisabled() {
897+
ctx.NotFound("Repo unit code is disabled", nil)
898+
return
899+
}
900+
}, explore.Code)
901+
})
893902
m.Get("/signing-key.gpg", misc.SigningKey)
894903
m.Post("/markup", reqToken(), bind(api.MarkupOption{}), misc.Markup)
895904
m.Post("/markdown", reqToken(), bind(api.MarkdownOption{}), misc.Markdown)

routers/api/v1/explore/code.go

Lines changed: 117 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,117 @@
1+
// Copyright 2021 The Gitea Authors. All rights reserved.
2+
// SPDX-License-Identifier: MIT
3+
4+
package explore
5+
6+
import (
7+
"net/http"
8+
9+
"code.gitea.io/gitea/models/db"
10+
repo_model "code.gitea.io/gitea/models/repo"
11+
code_indexer "code.gitea.io/gitea/modules/indexer/code"
12+
"code.gitea.io/gitea/modules/setting"
13+
api "code.gitea.io/gitea/modules/structs"
14+
"code.gitea.io/gitea/services/context"
15+
"code.gitea.io/gitea/services/convert"
16+
)
17+
18+
// Code explore code
19+
func Code(ctx *context.APIContext) {
20+
if !setting.Indexer.RepoIndexerEnabled {
21+
ctx.NotFound("Indexer not enabled")
22+
return
23+
}
24+
25+
language := ctx.FormTrim("l")
26+
keyword := ctx.FormTrim("q")
27+
28+
isFuzzy := ctx.FormOptionalBool("fuzzy").ValueOrDefault(true)
29+
30+
if keyword == "" {
31+
ctx.JSON(http.StatusInternalServerError, api.SearchError{OK: false, Error: "No keyword provided"})
32+
return
33+
}
34+
35+
page := ctx.FormInt("page")
36+
if page <= 0 {
37+
page = 1
38+
}
39+
40+
var (
41+
repoIDs []int64
42+
err error
43+
isAdmin bool
44+
)
45+
if ctx.Doer != nil {
46+
isAdmin = ctx.Doer.IsAdmin
47+
}
48+
49+
// guest user or non-admin user
50+
if ctx.Doer == nil || !isAdmin {
51+
repoIDs, err = repo_model.FindUserCodeAccessibleRepoIDs(ctx, ctx.Doer)
52+
if err != nil {
53+
ctx.ServerError("FindUserCodeAccessibleRepoIDs", err)
54+
return
55+
}
56+
}
57+
58+
var (
59+
total int
60+
searchResults []*code_indexer.Result
61+
repoMaps map[int64]*repo_model.Repository
62+
)
63+
64+
if (len(repoIDs) > 0) || isAdmin {
65+
total, searchResults, _, err = code_indexer.PerformSearch(ctx, &code_indexer.SearchOptions{
66+
RepoIDs: repoIDs,
67+
Keyword: keyword,
68+
IsKeywordFuzzy: isFuzzy,
69+
IsHtmlSafe: false,
70+
Language: language,
71+
Paginator: &db.ListOptions{
72+
Page: page,
73+
PageSize: setting.API.DefaultPagingNum,
74+
},
75+
})
76+
if err != nil {
77+
if code_indexer.IsAvailable(ctx) {
78+
ctx.ServerError("SearchResults", err)
79+
return
80+
}
81+
}
82+
83+
loadRepoIDs := make([]int64, 0, len(searchResults))
84+
for _, result := range searchResults {
85+
var find bool
86+
for _, id := range loadRepoIDs {
87+
if id == result.RepoID {
88+
find = true
89+
break
90+
}
91+
}
92+
if !find {
93+
loadRepoIDs = append(loadRepoIDs, result.RepoID)
94+
}
95+
}
96+
97+
repoMaps, err = repo_model.GetRepositoriesMapByIDs(ctx, loadRepoIDs)
98+
if err != nil {
99+
ctx.ServerError("GetRepositoriesMapByIDs", err)
100+
return
101+
}
102+
103+
if len(loadRepoIDs) != len(repoMaps) {
104+
// Remove deleted repos from search results
105+
cleanedSearchResults := make([]*code_indexer.Result, 0, len(repoMaps))
106+
for _, sr := range searchResults {
107+
if _, found := repoMaps[sr.RepoID]; found {
108+
cleanedSearchResults = append(cleanedSearchResults, sr)
109+
}
110+
}
111+
112+
searchResults = cleanedSearchResults
113+
}
114+
}
115+
116+
ctx.JSON(http.StatusOK, convert.ToExploreCodeSearchResults(total, searchResults, repoMaps))
117+
}

routers/web/explore/code.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -81,6 +81,7 @@ func Code(ctx *context.Context) {
8181
RepoIDs: repoIDs,
8282
Keyword: keyword,
8383
IsKeywordFuzzy: isFuzzy,
84+
IsHtmlSafe: true,
8485
Language: language,
8586
Paginator: &db.ListOptions{
8687
Page: page,

routers/web/repo/search.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,7 @@ func Search(ctx *context.Context) {
5959
RepoIDs: []int64{ctx.Repo.Repository.ID},
6060
Keyword: keyword,
6161
IsKeywordFuzzy: isFuzzy,
62+
IsHtmlSafe: true,
6263
Language: language,
6364
Paginator: &db.ListOptions{
6465
Page: page,

routers/web/user/code.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -80,6 +80,7 @@ func CodeSearch(ctx *context.Context) {
8080
Keyword: keyword,
8181
IsKeywordFuzzy: isFuzzy,
8282
Language: language,
83+
IsHtmlSafe: true,
8384
Paginator: &db.ListOptions{
8485
Page: page,
8586
PageSize: setting.UI.RepoSearchPagingNum,

services/convert/explore.go

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
// Copyright 2024 The Gitea Authors. All rights reserved.
2+
// SPDX-License-Identifier: MIT
3+
4+
package convert
5+
6+
import (
7+
repo_model "code.gitea.io/gitea/models/repo"
8+
code_indexer "code.gitea.io/gitea/modules/indexer/code"
9+
api "code.gitea.io/gitea/modules/structs"
10+
)
11+
12+
func ToExploreCodeSearchResults(total int, results []*code_indexer.Result, repoMaps map[int64]*repo_model.Repository) api.ExploreCodeResult {
13+
out := api.ExploreCodeResult{Total: total}
14+
for _, res := range results {
15+
if repo := repoMaps[res.RepoID]; repo != nil {
16+
for _, r := range res.Lines {
17+
out.Results = append(out.Results, api.ExploreCodeSearchItem{
18+
RepoName: repo.Name,
19+
FilePath: res.Filename,
20+
LineNumber: r.Num,
21+
LineText: r.RawContent,
22+
})
23+
}
24+
}
25+
}
26+
return out
27+
}

0 commit comments

Comments
 (0)