summaryrefslogtreecommitdiffstats
path: root/models
diff options
context:
space:
mode:
authorEthan Koenig <etk39@cornell.edu>2017-01-24 21:43:02 -0500
committerLunny Xiao <xiaolunwen@gmail.com>2017-01-25 10:43:02 +0800
commit833f8b94c2cd88277eba32984594aad2b7b2b05d (patch)
treead197af65043b654f0c64702db707b9335568bd7 /models
parent8bc431952f4ae76559054c3a9f41804b145d9230 (diff)
downloadgitea-833f8b94c2cd88277eba32984594aad2b7b2b05d.tar.gz
gitea-833f8b94c2cd88277eba32984594aad2b7b2b05d.zip
Search bar for issues/pulls (#530)
Diffstat (limited to 'models')
-rw-r--r--models/issue.go74
-rw-r--r--models/issue_comment.go18
-rw-r--r--models/issue_indexer.go183
-rw-r--r--models/models.go4
4 files changed, 252 insertions, 27 deletions
diff --git a/models/issue.go b/models/issue.go
index ac50d2dfba..d926161381 100644
--- a/models/issue.go
+++ b/models/issue.go
@@ -17,6 +17,7 @@ import (
"code.gitea.io/gitea/modules/base"
"code.gitea.io/gitea/modules/log"
"code.gitea.io/gitea/modules/setting"
+ "code.gitea.io/gitea/modules/util"
)
var (
@@ -451,8 +452,11 @@ func (issue *Issue) ReadBy(userID int64) error {
}
func updateIssueCols(e Engine, issue *Issue, cols ...string) error {
- _, err := e.Id(issue.ID).Cols(cols...).Update(issue)
- return err
+ if _, err := e.Id(issue.ID).Cols(cols...).Update(issue); err != nil {
+ return err
+ }
+ UpdateIssueIndexer(issue)
+ return nil
}
// UpdateIssueCols only updates values of specific columns for given issue.
@@ -733,6 +737,8 @@ func newIssue(e *xorm.Session, opts NewIssueOptions) (err error) {
return err
}
+ UpdateIssueIndexer(opts.Issue)
+
if len(opts.Attachments) > 0 {
attachments, err := getAttachmentsByUUIDs(e, opts.Attachments)
if err != nil {
@@ -865,10 +871,11 @@ type IssuesOptions struct {
MilestoneID int64
RepoIDs []int64
Page int
- IsClosed bool
- IsPull bool
+ IsClosed util.OptionalBool
+ IsPull util.OptionalBool
Labels string
SortType string
+ IssueIDs []int64
}
// sortIssuesSession sort an issues-related session based on the provided
@@ -894,11 +901,23 @@ func sortIssuesSession(sess *xorm.Session, sortType string) {
// Issues returns a list of issues by given conditions.
func Issues(opts *IssuesOptions) ([]*Issue, error) {
- if opts.Page <= 0 {
- opts.Page = 1
+ var sess *xorm.Session
+ if opts.Page >= 0 {
+ var start int
+ if opts.Page == 0 {
+ start = 0
+ } else {
+ start = (opts.Page - 1) * setting.UI.IssuePagingNum
+ }
+ sess = x.Limit(setting.UI.IssuePagingNum, start)
+ } else {
+ sess = x.NewSession()
+ defer sess.Close()
}
- sess := x.Limit(setting.UI.IssuePagingNum, (opts.Page-1)*setting.UI.IssuePagingNum)
+ if len(opts.IssueIDs) > 0 {
+ sess.In("issue.id", opts.IssueIDs)
+ }
if opts.RepoID > 0 {
sess.And("issue.repo_id=?", opts.RepoID)
@@ -906,7 +925,13 @@ func Issues(opts *IssuesOptions) ([]*Issue, error) {
// In case repository IDs are provided but actually no repository has issue.
sess.In("issue.repo_id", opts.RepoIDs)
}
- sess.And("issue.is_closed=?", opts.IsClosed)
+
+ switch opts.IsClosed {
+ case util.OptionalBoolTrue:
+ sess.And("issue.is_closed=true")
+ case util.OptionalBoolFalse:
+ sess.And("issue.is_closed=false")
+ }
if opts.AssigneeID > 0 {
sess.And("issue.assignee_id=?", opts.AssigneeID)
@@ -926,7 +951,12 @@ func Issues(opts *IssuesOptions) ([]*Issue, error) {
sess.And("issue.milestone_id=?", opts.MilestoneID)
}
- sess.And("issue.is_pull=?", opts.IsPull)
+ switch opts.IsPull {
+ case util.OptionalBoolTrue:
+ sess.And("issue.is_pull=true")
+ case util.OptionalBoolFalse:
+ sess.And("issue.is_pull=false")
+ }
sortIssuesSession(sess, opts.SortType)
@@ -1168,10 +1198,11 @@ type IssueStatsOptions struct {
MentionedID int64
PosterID int64
IsPull bool
+ IssueIDs []int64
}
// GetIssueStats returns issue statistic information by given conditions.
-func GetIssueStats(opts *IssueStatsOptions) *IssueStats {
+func GetIssueStats(opts *IssueStatsOptions) (*IssueStats, error) {
stats := &IssueStats{}
countSession := func(opts *IssueStatsOptions) *xorm.Session {
@@ -1179,6 +1210,10 @@ func GetIssueStats(opts *IssueStatsOptions) *IssueStats {
Where("issue.repo_id = ?", opts.RepoID).
And("is_pull = ?", opts.IsPull)
+ if len(opts.IssueIDs) > 0 {
+ sess.In("issue.id", opts.IssueIDs)
+ }
+
if len(opts.Labels) > 0 && opts.Labels != "0" {
labelIDs, err := base.StringsToInt64s(strings.Split(opts.Labels, ","))
if err != nil {
@@ -1210,13 +1245,20 @@ func GetIssueStats(opts *IssueStatsOptions) *IssueStats {
return sess
}
- stats.OpenCount, _ = countSession(opts).
+ var err error
+ stats.OpenCount, err = countSession(opts).
And("is_closed = ?", false).
Count(&Issue{})
- stats.ClosedCount, _ = countSession(opts).
+ if err != nil {
+ return nil, err
+ }
+ stats.ClosedCount, err = countSession(opts).
And("is_closed = ?", true).
Count(&Issue{})
- return stats
+ if err != nil {
+ return nil, err
+ }
+ return stats, nil
}
// GetUserIssueStats returns issue statistic information for dashboard by given conditions.
@@ -1294,7 +1336,11 @@ func GetRepoIssueStats(repoID, uid int64, filterMode int, isPull bool) (numOpen
func updateIssue(e Engine, issue *Issue) error {
_, err := e.Id(issue.ID).AllCols().Update(issue)
- return err
+ if err != nil {
+ return err
+ }
+ UpdateIssueIndexer(issue)
+ return nil
}
// UpdateIssue updates all fields of given issue.
diff --git a/models/issue_comment.go b/models/issue_comment.go
index e9a401b864..a17be97e72 100644
--- a/models/issue_comment.go
+++ b/models/issue_comment.go
@@ -454,28 +454,20 @@ func UpdateComment(c *Comment) error {
return err
}
-// DeleteCommentByID deletes the comment by given ID.
-func DeleteCommentByID(id int64) error {
- comment, err := GetCommentByID(id)
- if err != nil {
- if IsErrCommentNotExist(err) {
- return nil
- }
- return err
- }
-
+// DeleteComment deletes the comment
+func DeleteComment(comment *Comment) error {
sess := x.NewSession()
defer sessionRelease(sess)
- if err = sess.Begin(); err != nil {
+ if err := sess.Begin(); err != nil {
return err
}
- if _, err = sess.Id(comment.ID).Delete(new(Comment)); err != nil {
+ if _, err := sess.Id(comment.ID).Delete(new(Comment)); err != nil {
return err
}
if comment.Type == CommentTypeComment {
- if _, err = sess.Exec("UPDATE `issue` SET num_comments = num_comments - 1 WHERE id = ?", comment.IssueID); err != nil {
+ if _, err := sess.Exec("UPDATE `issue` SET num_comments = num_comments - 1 WHERE id = ?", comment.IssueID); err != nil {
return err
}
}
diff --git a/models/issue_indexer.go b/models/issue_indexer.go
new file mode 100644
index 0000000000..bbaf0e64bc
--- /dev/null
+++ b/models/issue_indexer.go
@@ -0,0 +1,183 @@
+// Copyright 2017 The Gitea Authors. All rights reserved.
+// Use of this source code is governed by a MIT-style
+// license that can be found in the LICENSE file.
+
+package models
+
+import (
+ "fmt"
+ "os"
+ "strconv"
+ "strings"
+
+ "code.gitea.io/gitea/modules/log"
+ "code.gitea.io/gitea/modules/setting"
+ "code.gitea.io/gitea/modules/util"
+ "github.com/blevesearch/bleve"
+ "github.com/blevesearch/bleve/analysis/analyzer/simple"
+ "github.com/blevesearch/bleve/search/query"
+)
+
+// issueIndexerUpdateQueue queue of issues that need to be updated in the issues
+// indexer
+var issueIndexerUpdateQueue chan *Issue
+
+// issueIndexer (thread-safe) index for searching issues
+var issueIndexer bleve.Index
+
+// issueIndexerData data stored in the issue indexer
+type issueIndexerData struct {
+ ID int64
+ RepoID int64
+
+ Title string
+ Content string
+}
+
+// numericQuery an numeric-equality query for the given value and field
+func numericQuery(value int64, field string) *query.NumericRangeQuery {
+ f := float64(value)
+ tru := true
+ q := bleve.NewNumericRangeInclusiveQuery(&f, &f, &tru, &tru)
+ q.SetField(field)
+ return q
+}
+
+// SearchIssuesByKeyword searches for issues by given conditions.
+// Returns the matching issue IDs
+func SearchIssuesByKeyword(repoID int64, keyword string) ([]int64, error) {
+ fields := strings.Fields(strings.ToLower(keyword))
+ indexerQuery := bleve.NewConjunctionQuery(
+ numericQuery(repoID, "RepoID"),
+ bleve.NewDisjunctionQuery(
+ bleve.NewPhraseQuery(fields, "Title"),
+ bleve.NewPhraseQuery(fields, "Content"),
+ ))
+ search := bleve.NewSearchRequestOptions(indexerQuery, 2147483647, 0, false)
+ search.Fields = []string{"ID"}
+
+ result, err := issueIndexer.Search(search)
+ if err != nil {
+ return nil, err
+ }
+
+ issueIDs := make([]int64, len(result.Hits))
+ for i, hit := range result.Hits {
+ issueIDs[i] = int64(hit.Fields["ID"].(float64))
+ }
+ return issueIDs, nil
+}
+
+// InitIssueIndexer initialize issue indexer
+func InitIssueIndexer() {
+ _, err := os.Stat(setting.Indexer.IssuePath)
+ if err != nil {
+ if os.IsNotExist(err) {
+ if err = createIssueIndexer(); err != nil {
+ log.Fatal(4, "CreateIssuesIndexer: %v", err)
+ }
+ if err = populateIssueIndexer(); err != nil {
+ log.Fatal(4, "PopulateIssuesIndex: %v", err)
+ }
+ } else {
+ log.Fatal(4, "InitIssuesIndexer: %v", err)
+ }
+ } else {
+ issueIndexer, err = bleve.Open(setting.Indexer.IssuePath)
+ if err != nil {
+ log.Fatal(4, "InitIssuesIndexer, open index: %v", err)
+ }
+ }
+ issueIndexerUpdateQueue = make(chan *Issue, setting.Indexer.UpdateQueueLength)
+ go processIssueIndexerUpdateQueue()
+ // TODO close issueIndexer when Gitea closes
+}
+
+// createIssueIndexer create an issue indexer if one does not already exist
+func createIssueIndexer() error {
+ mapping := bleve.NewIndexMapping()
+ docMapping := bleve.NewDocumentMapping()
+
+ docMapping.AddFieldMappingsAt("ID", bleve.NewNumericFieldMapping())
+ docMapping.AddFieldMappingsAt("RepoID", bleve.NewNumericFieldMapping())
+
+ textFieldMapping := bleve.NewTextFieldMapping()
+ textFieldMapping.Analyzer = simple.Name
+ docMapping.AddFieldMappingsAt("Title", textFieldMapping)
+ docMapping.AddFieldMappingsAt("Content", textFieldMapping)
+
+ mapping.AddDocumentMapping("issues", docMapping)
+
+ var err error
+ issueIndexer, err = bleve.New(setting.Indexer.IssuePath, mapping)
+ return err
+}
+
+// populateIssueIndexer populate the issue indexer with issue data
+func populateIssueIndexer() error {
+ for page := 1; ; page++ {
+ repos, err := Repositories(&SearchRepoOptions{
+ Page: page,
+ PageSize: 10,
+ })
+ if err != nil {
+ return fmt.Errorf("Repositories: %v", err)
+ }
+ if len(repos) == 0 {
+ return nil
+ }
+ batch := issueIndexer.NewBatch()
+ for _, repo := range repos {
+ issues, err := Issues(&IssuesOptions{
+ RepoID: repo.ID,
+ IsClosed: util.OptionalBoolNone,
+ IsPull: util.OptionalBoolNone,
+ Page: -1, // do not page
+ })
+ if err != nil {
+ return fmt.Errorf("Issues: %v", err)
+ }
+ for _, issue := range issues {
+ err = batch.Index(issue.indexUID(), issue.issueData())
+ if err != nil {
+ return fmt.Errorf("batch.Index: %v", err)
+ }
+ }
+ }
+ if err = issueIndexer.Batch(batch); err != nil {
+ return fmt.Errorf("index.Batch: %v", err)
+ }
+ }
+}
+
+func processIssueIndexerUpdateQueue() {
+ for {
+ select {
+ case issue := <-issueIndexerUpdateQueue:
+ if err := issueIndexer.Index(issue.indexUID(), issue.issueData()); err != nil {
+ log.Error(4, "issuesIndexer.Index: %v", err)
+ }
+ }
+ }
+}
+
+// indexUID a unique identifier for an issue used in full-text indices
+func (issue *Issue) indexUID() string {
+ return strconv.FormatInt(issue.ID, 36)
+}
+
+func (issue *Issue) issueData() *issueIndexerData {
+ return &issueIndexerData{
+ ID: issue.ID,
+ RepoID: issue.RepoID,
+ Title: issue.Title,
+ Content: issue.Content,
+ }
+}
+
+// UpdateIssueIndexer add/update an issue to the issue indexer
+func UpdateIssueIndexer(issue *Issue) {
+ go func() {
+ issueIndexerUpdateQueue <- issue
+ }()
+}
diff --git a/models/models.go b/models/models.go
index d9716e79bd..1ce704a9e4 100644
--- a/models/models.go
+++ b/models/models.go
@@ -138,6 +138,10 @@ func LoadConfigs() {
}
DbCfg.SSLMode = sec.Key("SSL_MODE").String()
DbCfg.Path = sec.Key("PATH").MustString("data/gitea.db")
+
+ sec = setting.Cfg.Section("indexer")
+ setting.Indexer.IssuePath = sec.Key("ISSUE_INDEXER_PATH").MustString("indexers/issues.bleve")
+ setting.Indexer.UpdateQueueLength = sec.Key("UPDATE_BUFFER_LEN").MustInt(20)
}
// parsePostgreSQLHostPort parses given input in various forms defined in