Skip to content

Commit 833f8b9

Browse files
ethantkoeniglunny
authored andcommitted
Search bar for issues/pulls (#530)
1 parent 8bc4319 commit 833f8b9

File tree

195 files changed

+221830
-60
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

195 files changed

+221830
-60
lines changed

.gitignore

+1
Original file line numberDiff line numberDiff line change
@@ -41,5 +41,6 @@ coverage.out
4141
/dist
4242
/custom
4343
/data
44+
/indexers
4445
/log
4546
/public/img/avatar

conf/app.ini

+4
Original file line numberDiff line numberDiff line change
@@ -158,6 +158,10 @@ SSL_MODE = disable
158158
; For "sqlite3" and "tidb", use absolute path when you start as service
159159
PATH = data/gitea.db
160160

161+
[indexer]
162+
ISSUE_INDEXER_PATH = indexers/issues.bleve
163+
UPDATE_BUFFER_LEN = 20
164+
161165
[admin]
162166

163167
[security]

models/issue.go

+60-14
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ import (
1717
"code.gitea.io/gitea/modules/base"
1818
"code.gitea.io/gitea/modules/log"
1919
"code.gitea.io/gitea/modules/setting"
20+
"code.gitea.io/gitea/modules/util"
2021
)
2122

2223
var (
@@ -451,8 +452,11 @@ func (issue *Issue) ReadBy(userID int64) error {
451452
}
452453

453454
func updateIssueCols(e Engine, issue *Issue, cols ...string) error {
454-
_, err := e.Id(issue.ID).Cols(cols...).Update(issue)
455-
return err
455+
if _, err := e.Id(issue.ID).Cols(cols...).Update(issue); err != nil {
456+
return err
457+
}
458+
UpdateIssueIndexer(issue)
459+
return nil
456460
}
457461

458462
// UpdateIssueCols only updates values of specific columns for given issue.
@@ -733,6 +737,8 @@ func newIssue(e *xorm.Session, opts NewIssueOptions) (err error) {
733737
return err
734738
}
735739

740+
UpdateIssueIndexer(opts.Issue)
741+
736742
if len(opts.Attachments) > 0 {
737743
attachments, err := getAttachmentsByUUIDs(e, opts.Attachments)
738744
if err != nil {
@@ -865,10 +871,11 @@ type IssuesOptions struct {
865871
MilestoneID int64
866872
RepoIDs []int64
867873
Page int
868-
IsClosed bool
869-
IsPull bool
874+
IsClosed util.OptionalBool
875+
IsPull util.OptionalBool
870876
Labels string
871877
SortType string
878+
IssueIDs []int64
872879
}
873880

874881
// sortIssuesSession sort an issues-related session based on the provided
@@ -894,19 +901,37 @@ func sortIssuesSession(sess *xorm.Session, sortType string) {
894901

895902
// Issues returns a list of issues by given conditions.
896903
func Issues(opts *IssuesOptions) ([]*Issue, error) {
897-
if opts.Page <= 0 {
898-
opts.Page = 1
904+
var sess *xorm.Session
905+
if opts.Page >= 0 {
906+
var start int
907+
if opts.Page == 0 {
908+
start = 0
909+
} else {
910+
start = (opts.Page - 1) * setting.UI.IssuePagingNum
911+
}
912+
sess = x.Limit(setting.UI.IssuePagingNum, start)
913+
} else {
914+
sess = x.NewSession()
915+
defer sess.Close()
899916
}
900917

901-
sess := x.Limit(setting.UI.IssuePagingNum, (opts.Page-1)*setting.UI.IssuePagingNum)
918+
if len(opts.IssueIDs) > 0 {
919+
sess.In("issue.id", opts.IssueIDs)
920+
}
902921

903922
if opts.RepoID > 0 {
904923
sess.And("issue.repo_id=?", opts.RepoID)
905924
} else if len(opts.RepoIDs) > 0 {
906925
// In case repository IDs are provided but actually no repository has issue.
907926
sess.In("issue.repo_id", opts.RepoIDs)
908927
}
909-
sess.And("issue.is_closed=?", opts.IsClosed)
928+
929+
switch opts.IsClosed {
930+
case util.OptionalBoolTrue:
931+
sess.And("issue.is_closed=true")
932+
case util.OptionalBoolFalse:
933+
sess.And("issue.is_closed=false")
934+
}
910935

911936
if opts.AssigneeID > 0 {
912937
sess.And("issue.assignee_id=?", opts.AssigneeID)
@@ -926,7 +951,12 @@ func Issues(opts *IssuesOptions) ([]*Issue, error) {
926951
sess.And("issue.milestone_id=?", opts.MilestoneID)
927952
}
928953

929-
sess.And("issue.is_pull=?", opts.IsPull)
954+
switch opts.IsPull {
955+
case util.OptionalBoolTrue:
956+
sess.And("issue.is_pull=true")
957+
case util.OptionalBoolFalse:
958+
sess.And("issue.is_pull=false")
959+
}
930960

931961
sortIssuesSession(sess, opts.SortType)
932962

@@ -1168,17 +1198,22 @@ type IssueStatsOptions struct {
11681198
MentionedID int64
11691199
PosterID int64
11701200
IsPull bool
1201+
IssueIDs []int64
11711202
}
11721203

11731204
// GetIssueStats returns issue statistic information by given conditions.
1174-
func GetIssueStats(opts *IssueStatsOptions) *IssueStats {
1205+
func GetIssueStats(opts *IssueStatsOptions) (*IssueStats, error) {
11751206
stats := &IssueStats{}
11761207

11771208
countSession := func(opts *IssueStatsOptions) *xorm.Session {
11781209
sess := x.
11791210
Where("issue.repo_id = ?", opts.RepoID).
11801211
And("is_pull = ?", opts.IsPull)
11811212

1213+
if len(opts.IssueIDs) > 0 {
1214+
sess.In("issue.id", opts.IssueIDs)
1215+
}
1216+
11821217
if len(opts.Labels) > 0 && opts.Labels != "0" {
11831218
labelIDs, err := base.StringsToInt64s(strings.Split(opts.Labels, ","))
11841219
if err != nil {
@@ -1210,13 +1245,20 @@ func GetIssueStats(opts *IssueStatsOptions) *IssueStats {
12101245
return sess
12111246
}
12121247

1213-
stats.OpenCount, _ = countSession(opts).
1248+
var err error
1249+
stats.OpenCount, err = countSession(opts).
12141250
And("is_closed = ?", false).
12151251
Count(&Issue{})
1216-
stats.ClosedCount, _ = countSession(opts).
1252+
if err != nil {
1253+
return nil, err
1254+
}
1255+
stats.ClosedCount, err = countSession(opts).
12171256
And("is_closed = ?", true).
12181257
Count(&Issue{})
1219-
return stats
1258+
if err != nil {
1259+
return nil, err
1260+
}
1261+
return stats, nil
12201262
}
12211263

12221264
// GetUserIssueStats returns issue statistic information for dashboard by given conditions.
@@ -1294,7 +1336,11 @@ func GetRepoIssueStats(repoID, uid int64, filterMode int, isPull bool) (numOpen
12941336

12951337
func updateIssue(e Engine, issue *Issue) error {
12961338
_, err := e.Id(issue.ID).AllCols().Update(issue)
1297-
return err
1339+
if err != nil {
1340+
return err
1341+
}
1342+
UpdateIssueIndexer(issue)
1343+
return nil
12981344
}
12991345

13001346
// UpdateIssue updates all fields of given issue.

models/issue_comment.go

+5-13
Original file line numberDiff line numberDiff line change
@@ -454,28 +454,20 @@ func UpdateComment(c *Comment) error {
454454
return err
455455
}
456456

457-
// DeleteCommentByID deletes the comment by given ID.
458-
func DeleteCommentByID(id int64) error {
459-
comment, err := GetCommentByID(id)
460-
if err != nil {
461-
if IsErrCommentNotExist(err) {
462-
return nil
463-
}
464-
return err
465-
}
466-
457+
// DeleteComment deletes the comment
458+
func DeleteComment(comment *Comment) error {
467459
sess := x.NewSession()
468460
defer sessionRelease(sess)
469-
if err = sess.Begin(); err != nil {
461+
if err := sess.Begin(); err != nil {
470462
return err
471463
}
472464

473-
if _, err = sess.Id(comment.ID).Delete(new(Comment)); err != nil {
465+
if _, err := sess.Id(comment.ID).Delete(new(Comment)); err != nil {
474466
return err
475467
}
476468

477469
if comment.Type == CommentTypeComment {
478-
if _, err = sess.Exec("UPDATE `issue` SET num_comments = num_comments - 1 WHERE id = ?", comment.IssueID); err != nil {
470+
if _, err := sess.Exec("UPDATE `issue` SET num_comments = num_comments - 1 WHERE id = ?", comment.IssueID); err != nil {
479471
return err
480472
}
481473
}

models/issue_indexer.go

+183
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,183 @@
1+
// Copyright 2017 The Gitea Authors. All rights reserved.
2+
// Use of this source code is governed by a MIT-style
3+
// license that can be found in the LICENSE file.
4+
5+
package models
6+
7+
import (
8+
"fmt"
9+
"os"
10+
"strconv"
11+
"strings"
12+
13+
"code.gitea.io/gitea/modules/log"
14+
"code.gitea.io/gitea/modules/setting"
15+
"code.gitea.io/gitea/modules/util"
16+
"github.com/blevesearch/bleve"
17+
"github.com/blevesearch/bleve/analysis/analyzer/simple"
18+
"github.com/blevesearch/bleve/search/query"
19+
)
20+
21+
// issueIndexerUpdateQueue queue of issues that need to be updated in the issues
22+
// indexer
23+
var issueIndexerUpdateQueue chan *Issue
24+
25+
// issueIndexer (thread-safe) index for searching issues
26+
var issueIndexer bleve.Index
27+
28+
// issueIndexerData data stored in the issue indexer
29+
type issueIndexerData struct {
30+
ID int64
31+
RepoID int64
32+
33+
Title string
34+
Content string
35+
}
36+
37+
// numericQuery an numeric-equality query for the given value and field
38+
func numericQuery(value int64, field string) *query.NumericRangeQuery {
39+
f := float64(value)
40+
tru := true
41+
q := bleve.NewNumericRangeInclusiveQuery(&f, &f, &tru, &tru)
42+
q.SetField(field)
43+
return q
44+
}
45+
46+
// SearchIssuesByKeyword searches for issues by given conditions.
47+
// Returns the matching issue IDs
48+
func SearchIssuesByKeyword(repoID int64, keyword string) ([]int64, error) {
49+
fields := strings.Fields(strings.ToLower(keyword))
50+
indexerQuery := bleve.NewConjunctionQuery(
51+
numericQuery(repoID, "RepoID"),
52+
bleve.NewDisjunctionQuery(
53+
bleve.NewPhraseQuery(fields, "Title"),
54+
bleve.NewPhraseQuery(fields, "Content"),
55+
))
56+
search := bleve.NewSearchRequestOptions(indexerQuery, 2147483647, 0, false)
57+
search.Fields = []string{"ID"}
58+
59+
result, err := issueIndexer.Search(search)
60+
if err != nil {
61+
return nil, err
62+
}
63+
64+
issueIDs := make([]int64, len(result.Hits))
65+
for i, hit := range result.Hits {
66+
issueIDs[i] = int64(hit.Fields["ID"].(float64))
67+
}
68+
return issueIDs, nil
69+
}
70+
71+
// InitIssueIndexer initialize issue indexer
72+
func InitIssueIndexer() {
73+
_, err := os.Stat(setting.Indexer.IssuePath)
74+
if err != nil {
75+
if os.IsNotExist(err) {
76+
if err = createIssueIndexer(); err != nil {
77+
log.Fatal(4, "CreateIssuesIndexer: %v", err)
78+
}
79+
if err = populateIssueIndexer(); err != nil {
80+
log.Fatal(4, "PopulateIssuesIndex: %v", err)
81+
}
82+
} else {
83+
log.Fatal(4, "InitIssuesIndexer: %v", err)
84+
}
85+
} else {
86+
issueIndexer, err = bleve.Open(setting.Indexer.IssuePath)
87+
if err != nil {
88+
log.Fatal(4, "InitIssuesIndexer, open index: %v", err)
89+
}
90+
}
91+
issueIndexerUpdateQueue = make(chan *Issue, setting.Indexer.UpdateQueueLength)
92+
go processIssueIndexerUpdateQueue()
93+
// TODO close issueIndexer when Gitea closes
94+
}
95+
96+
// createIssueIndexer create an issue indexer if one does not already exist
97+
func createIssueIndexer() error {
98+
mapping := bleve.NewIndexMapping()
99+
docMapping := bleve.NewDocumentMapping()
100+
101+
docMapping.AddFieldMappingsAt("ID", bleve.NewNumericFieldMapping())
102+
docMapping.AddFieldMappingsAt("RepoID", bleve.NewNumericFieldMapping())
103+
104+
textFieldMapping := bleve.NewTextFieldMapping()
105+
textFieldMapping.Analyzer = simple.Name
106+
docMapping.AddFieldMappingsAt("Title", textFieldMapping)
107+
docMapping.AddFieldMappingsAt("Content", textFieldMapping)
108+
109+
mapping.AddDocumentMapping("issues", docMapping)
110+
111+
var err error
112+
issueIndexer, err = bleve.New(setting.Indexer.IssuePath, mapping)
113+
return err
114+
}
115+
116+
// populateIssueIndexer populate the issue indexer with issue data
117+
func populateIssueIndexer() error {
118+
for page := 1; ; page++ {
119+
repos, err := Repositories(&SearchRepoOptions{
120+
Page: page,
121+
PageSize: 10,
122+
})
123+
if err != nil {
124+
return fmt.Errorf("Repositories: %v", err)
125+
}
126+
if len(repos) == 0 {
127+
return nil
128+
}
129+
batch := issueIndexer.NewBatch()
130+
for _, repo := range repos {
131+
issues, err := Issues(&IssuesOptions{
132+
RepoID: repo.ID,
133+
IsClosed: util.OptionalBoolNone,
134+
IsPull: util.OptionalBoolNone,
135+
Page: -1, // do not page
136+
})
137+
if err != nil {
138+
return fmt.Errorf("Issues: %v", err)
139+
}
140+
for _, issue := range issues {
141+
err = batch.Index(issue.indexUID(), issue.issueData())
142+
if err != nil {
143+
return fmt.Errorf("batch.Index: %v", err)
144+
}
145+
}
146+
}
147+
if err = issueIndexer.Batch(batch); err != nil {
148+
return fmt.Errorf("index.Batch: %v", err)
149+
}
150+
}
151+
}
152+
153+
func processIssueIndexerUpdateQueue() {
154+
for {
155+
select {
156+
case issue := <-issueIndexerUpdateQueue:
157+
if err := issueIndexer.Index(issue.indexUID(), issue.issueData()); err != nil {
158+
log.Error(4, "issuesIndexer.Index: %v", err)
159+
}
160+
}
161+
}
162+
}
163+
164+
// indexUID a unique identifier for an issue used in full-text indices
165+
func (issue *Issue) indexUID() string {
166+
return strconv.FormatInt(issue.ID, 36)
167+
}
168+
169+
func (issue *Issue) issueData() *issueIndexerData {
170+
return &issueIndexerData{
171+
ID: issue.ID,
172+
RepoID: issue.RepoID,
173+
Title: issue.Title,
174+
Content: issue.Content,
175+
}
176+
}
177+
178+
// UpdateIssueIndexer add/update an issue to the issue indexer
179+
func UpdateIssueIndexer(issue *Issue) {
180+
go func() {
181+
issueIndexerUpdateQueue <- issue
182+
}()
183+
}

0 commit comments

Comments
 (0)