first-commit
This commit is contained in:
143
modules/indexer/issues/db/db.go
Normal file
143
modules/indexer/issues/db/db.go
Normal file
@@ -0,0 +1,143 @@
|
||||
// Copyright 2019 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package db
|
||||
|
||||
import (
|
||||
"context"
|
||||
"strings"
|
||||
"sync"
|
||||
|
||||
"code.gitea.io/gitea/models/db"
|
||||
issue_model "code.gitea.io/gitea/models/issues"
|
||||
"code.gitea.io/gitea/modules/indexer"
|
||||
indexer_internal "code.gitea.io/gitea/modules/indexer/internal"
|
||||
inner_db "code.gitea.io/gitea/modules/indexer/internal/db"
|
||||
"code.gitea.io/gitea/modules/indexer/issues/internal"
|
||||
"code.gitea.io/gitea/modules/util"
|
||||
|
||||
"xorm.io/builder"
|
||||
)
|
||||
|
||||
var _ internal.Indexer = (*Indexer)(nil)
|
||||
|
||||
// Indexer implements Indexer interface to use database's like search
|
||||
type Indexer struct {
|
||||
indexer_internal.Indexer
|
||||
}
|
||||
|
||||
func (i *Indexer) SupportedSearchModes() []indexer.SearchMode {
|
||||
return indexer.SearchModesExactWords()
|
||||
}
|
||||
|
||||
var GetIndexer = sync.OnceValue(func() *Indexer {
|
||||
return &Indexer{Indexer: &inner_db.Indexer{}}
|
||||
})
|
||||
|
||||
// Index dummy function
|
||||
func (i *Indexer) Index(_ context.Context, _ ...*internal.IndexerData) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Delete dummy function
|
||||
func (i *Indexer) Delete(_ context.Context, _ ...int64) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func buildMatchQuery(mode indexer.SearchModeType, colName, keyword string) builder.Cond {
|
||||
if mode == indexer.SearchModeExact {
|
||||
return db.BuildCaseInsensitiveLike(colName, keyword)
|
||||
}
|
||||
|
||||
// match words
|
||||
cond := builder.NewCond()
|
||||
fields := strings.Fields(keyword)
|
||||
if len(fields) == 0 {
|
||||
return builder.Expr("1=1")
|
||||
}
|
||||
for _, field := range fields {
|
||||
if field == "" {
|
||||
continue
|
||||
}
|
||||
cond = cond.And(db.BuildCaseInsensitiveLike(colName, field))
|
||||
}
|
||||
return cond
|
||||
}
|
||||
|
||||
// Search searches for issues
|
||||
func (i *Indexer) Search(ctx context.Context, options *internal.SearchOptions) (*internal.SearchResult, error) {
|
||||
// FIXME: I tried to avoid importing models here, but it seems to be impossible.
|
||||
// We can provide a function to register the search function, so models/issues can register it.
|
||||
// So models/issues will import modules/indexer/issues, it's OK because it's by design.
|
||||
// But modules/indexer/issues has already imported models/issues to do UpdateRepoIndexer and UpdateIssueIndexer.
|
||||
// And to avoid circular import, we have to move the functions to another package.
|
||||
// I believe it should be services/indexer, sounds great!
|
||||
// But the two functions are used in modules/notification/indexer, that means we will import services/indexer in modules/notification/indexer.
|
||||
// So that's the root problem:
|
||||
// The notification is defined in modules, but it's using lots of things should be in services.
|
||||
|
||||
cond := builder.NewCond()
|
||||
|
||||
if options.Keyword != "" {
|
||||
repoCond := builder.In("repo_id", options.RepoIDs)
|
||||
if len(options.RepoIDs) == 1 {
|
||||
repoCond = builder.Eq{"repo_id": options.RepoIDs[0]}
|
||||
}
|
||||
subQuery := builder.Select("id").From("issue").Where(repoCond)
|
||||
searchMode := util.IfZero(options.SearchMode, i.SupportedSearchModes()[0].ModeValue)
|
||||
cond = builder.Or(
|
||||
buildMatchQuery(searchMode, "issue.name", options.Keyword),
|
||||
buildMatchQuery(searchMode, "issue.content", options.Keyword),
|
||||
builder.In("issue.id", builder.Select("issue_id").
|
||||
From("comment").
|
||||
Where(builder.And(
|
||||
builder.Eq{"type": issue_model.CommentTypeComment},
|
||||
builder.In("issue_id", subQuery),
|
||||
buildMatchQuery(searchMode, "content", options.Keyword),
|
||||
)),
|
||||
),
|
||||
)
|
||||
|
||||
if options.IsKeywordNumeric() {
|
||||
cond = cond.Or(
|
||||
builder.Eq{"`index`": options.Keyword},
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
opt, err := ToDBOptions(ctx, options)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// If pagesize == 0, return total count only. It's a special case for search count.
|
||||
if options.Paginator != nil && options.Paginator.PageSize == 0 {
|
||||
total, err := issue_model.CountIssues(ctx, opt, cond)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &internal.SearchResult{
|
||||
Total: total,
|
||||
}, nil
|
||||
}
|
||||
|
||||
return i.FindWithIssueOptions(ctx, opt, cond)
|
||||
}
|
||||
|
||||
func (i *Indexer) FindWithIssueOptions(ctx context.Context, opt *issue_model.IssuesOptions, otherConds ...builder.Cond) (*internal.SearchResult, error) {
|
||||
ids, total, err := issue_model.IssueIDs(ctx, opt, otherConds...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
hits := make([]internal.Match, 0, len(ids))
|
||||
for _, id := range ids {
|
||||
hits = append(hits, internal.Match{
|
||||
ID: id,
|
||||
})
|
||||
}
|
||||
return &internal.SearchResult{
|
||||
Total: total,
|
||||
Hits: hits,
|
||||
}, nil
|
||||
}
|
116
modules/indexer/issues/db/options.go
Normal file
116
modules/indexer/issues/db/options.go
Normal file
@@ -0,0 +1,116 @@
|
||||
// Copyright 2023 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package db
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"code.gitea.io/gitea/models/db"
|
||||
issue_model "code.gitea.io/gitea/models/issues"
|
||||
"code.gitea.io/gitea/modules/container"
|
||||
"code.gitea.io/gitea/modules/indexer/issues/internal"
|
||||
"code.gitea.io/gitea/modules/optional"
|
||||
)
|
||||
|
||||
func ToDBOptions(ctx context.Context, options *internal.SearchOptions) (*issue_model.IssuesOptions, error) {
|
||||
var sortType string
|
||||
switch options.SortBy {
|
||||
case internal.SortByCreatedAsc:
|
||||
sortType = "oldest"
|
||||
case internal.SortByUpdatedAsc:
|
||||
sortType = "leastupdate"
|
||||
case internal.SortByCommentsAsc:
|
||||
sortType = "leastcomment"
|
||||
case internal.SortByDeadlineDesc:
|
||||
sortType = "farduedate"
|
||||
case internal.SortByCreatedDesc:
|
||||
sortType = "newest"
|
||||
case internal.SortByUpdatedDesc:
|
||||
sortType = "recentupdate"
|
||||
case internal.SortByCommentsDesc:
|
||||
sortType = "mostcomment"
|
||||
case internal.SortByDeadlineAsc:
|
||||
sortType = "nearduedate"
|
||||
default:
|
||||
if strings.HasPrefix(string(options.SortBy), issue_model.ScopeSortPrefix) {
|
||||
sortType = string(options.SortBy)
|
||||
} else {
|
||||
sortType = "newest"
|
||||
}
|
||||
}
|
||||
|
||||
// See the comment of issues_model.SearchOptions for the reason why we need to convert
|
||||
convertID := func(id optional.Option[int64]) int64 {
|
||||
if !id.Has() {
|
||||
return 0
|
||||
}
|
||||
value := id.Value()
|
||||
if value == 0 {
|
||||
return db.NoConditionID
|
||||
}
|
||||
return value
|
||||
}
|
||||
|
||||
opts := &issue_model.IssuesOptions{
|
||||
Paginator: options.Paginator,
|
||||
RepoIDs: options.RepoIDs,
|
||||
AllPublic: options.AllPublic,
|
||||
RepoCond: nil,
|
||||
AssigneeID: options.AssigneeID,
|
||||
PosterID: options.PosterID,
|
||||
MentionedID: convertID(options.MentionID),
|
||||
ReviewRequestedID: convertID(options.ReviewRequestedID),
|
||||
ReviewedID: convertID(options.ReviewedID),
|
||||
SubscriberID: convertID(options.SubscriberID),
|
||||
ProjectID: convertID(options.ProjectID),
|
||||
ProjectColumnID: convertID(options.ProjectColumnID),
|
||||
IsClosed: options.IsClosed,
|
||||
IsPull: options.IsPull,
|
||||
IncludedLabelNames: nil,
|
||||
ExcludedLabelNames: nil,
|
||||
IncludeMilestones: nil,
|
||||
SortType: sortType,
|
||||
UpdatedAfterUnix: options.UpdatedAfterUnix.Value(),
|
||||
UpdatedBeforeUnix: options.UpdatedBeforeUnix.Value(),
|
||||
PriorityRepoID: 0,
|
||||
IsArchived: options.IsArchived,
|
||||
Owner: nil,
|
||||
Team: nil,
|
||||
Doer: nil,
|
||||
}
|
||||
|
||||
if len(options.MilestoneIDs) == 1 && options.MilestoneIDs[0] == 0 {
|
||||
opts.MilestoneIDs = []int64{db.NoConditionID}
|
||||
} else {
|
||||
opts.MilestoneIDs = options.MilestoneIDs
|
||||
}
|
||||
|
||||
if options.NoLabelOnly {
|
||||
opts.LabelIDs = []int64{0} // Be careful, it's zero, not db.NoConditionID
|
||||
} else {
|
||||
opts.LabelIDs = make([]int64, 0, len(options.IncludedLabelIDs)+len(options.ExcludedLabelIDs))
|
||||
opts.LabelIDs = append(opts.LabelIDs, options.IncludedLabelIDs...)
|
||||
for _, id := range options.ExcludedLabelIDs {
|
||||
opts.LabelIDs = append(opts.LabelIDs, -id)
|
||||
}
|
||||
|
||||
if len(options.IncludedLabelIDs) == 0 && len(options.IncludedAnyLabelIDs) > 0 {
|
||||
labels, err := issue_model.GetLabelsByIDs(ctx, options.IncludedAnyLabelIDs, "name")
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("GetLabelsByIDs: %v", err)
|
||||
}
|
||||
set := container.Set[string]{}
|
||||
for _, label := range labels {
|
||||
if !set.Contains(label.Name) {
|
||||
set.Add(label.Name)
|
||||
opts.IncludedLabelNames = append(opts.IncludedLabelNames, label.Name)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return opts, nil
|
||||
}
|
Reference in New Issue
Block a user