Skip to content

Commit 82be59e

Browse files
issue search on my related repositories (#9758)
* adding search capability to user's issues dashboard * global issue search * placement of search bar on issues dashboard * fixed some bugs in the issue dashboard search * added unit test because IssueIDs option was added to UserIssueStatsOptions * some renaming of fields in the issue dashboard code to be more clear; also trying to fix issue of searching the right repos based on the filter * added unit test fro GetRepoIDsForIssuesOptions; fixed search lost on pagination; using shown issue status for open/close count; removed some debugging * fix issue with all count showing incorrectly * removed todo comment left in by mistake * typo pulling wrong count * fxied all count being off when selecting repositories * setting the opts.IsClosed after pulling repos to search, this is done so that the list of repo ids to serach for the keyword is not limited, we need to get all the issue ids for the shown issue stats * added "accessibleRepositoryCondition" check on the query to pull the repo ids to search for issues, this is an added protection to ensure we don't search repos the user does not have access to * added code so that in the issues search, we won't use an in clause of issues ids that goes over 1000 * fixed unit test * using 950 as the limit for issue search, removed unneeded group by in GetRepoIDsForIssuesOptions, showing search on pulls dashboard page too (not just issues) Co-authored-by: guillep2k <[email protected]>
1 parent af61b22 commit 82be59e

File tree

4 files changed

+214
-54
lines changed

4 files changed

+214
-54
lines changed

models/issue.go

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -76,6 +76,7 @@ var (
7676
const issueTasksRegexpStr = `(^\s*[-*]\s\[[\sx]\]\s.)|(\n\s*[-*]\s\[[\sx]\]\s.)`
7777
const issueTasksDoneRegexpStr = `(^\s*[-*]\s\[[x]\]\s.)|(\n\s*[-*]\s\[[x]\]\s.)`
7878
const issueMaxDupIndexAttempts = 3
79+
const maxIssueIDs = 950
7980

8081
func init() {
8182
issueTasksPat = regexp.MustCompile(issueTasksRegexpStr)
@@ -1098,6 +1099,9 @@ func (opts *IssuesOptions) setupSession(sess *xorm.Session) {
10981099
}
10991100

11001101
if len(opts.IssueIDs) > 0 {
1102+
if len(opts.IssueIDs) > maxIssueIDs {
1103+
opts.IssueIDs = opts.IssueIDs[:maxIssueIDs]
1104+
}
11011105
sess.In("issue.id", opts.IssueIDs)
11021106
}
11031107

@@ -1176,6 +1180,26 @@ func CountIssuesByRepo(opts *IssuesOptions) (map[int64]int64, error) {
11761180
return countMap, nil
11771181
}
11781182

1183+
// GetRepoIDsForIssuesOptions find all repo ids for the given options
1184+
func GetRepoIDsForIssuesOptions(opts *IssuesOptions, user *User) ([]int64, error) {
1185+
repoIDs := make([]int64, 0, 5)
1186+
sess := x.NewSession()
1187+
defer sess.Close()
1188+
1189+
opts.setupSession(sess)
1190+
1191+
accessCond := accessibleRepositoryCondition(user)
1192+
if err := sess.Where(accessCond).
1193+
Join("INNER", "repository", "`issue`.repo_id = `repository`.id").
1194+
Distinct("issue.repo_id").
1195+
Table("issue").
1196+
Find(&repoIDs); err != nil {
1197+
return nil, err
1198+
}
1199+
1200+
return repoIDs, nil
1201+
}
1202+
11791203
// Issues returns a list of issues by given conditions.
11801204
func Issues(opts *IssuesOptions) ([]*Issue, error) {
11811205
sess := x.NewSession()
@@ -1313,6 +1337,9 @@ func getIssueStatsChunk(opts *IssueStatsOptions, issueIDs []int64) (*IssueStats,
13131337
Where("issue.repo_id = ?", opts.RepoID)
13141338

13151339
if len(opts.IssueIDs) > 0 {
1340+
if len(opts.IssueIDs) > maxIssueIDs {
1341+
opts.IssueIDs = opts.IssueIDs[:maxIssueIDs]
1342+
}
13161343
sess.In("issue.id", opts.IssueIDs)
13171344
}
13181345

@@ -1382,6 +1409,7 @@ type UserIssueStatsOptions struct {
13821409
FilterMode int
13831410
IsPull bool
13841411
IsClosed bool
1412+
IssueIDs []int64
13851413
}
13861414

13871415
// GetUserIssueStats returns issue statistic information for dashboard by given conditions.
@@ -1394,6 +1422,12 @@ func GetUserIssueStats(opts UserIssueStatsOptions) (*IssueStats, error) {
13941422
if len(opts.RepoIDs) > 0 {
13951423
cond = cond.And(builder.In("issue.repo_id", opts.RepoIDs))
13961424
}
1425+
if len(opts.IssueIDs) > 0 {
1426+
if len(opts.IssueIDs) > maxIssueIDs {
1427+
opts.IssueIDs = opts.IssueIDs[:maxIssueIDs]
1428+
}
1429+
cond = cond.And(builder.In("issue.id", opts.IssueIDs))
1430+
}
13971431

13981432
switch opts.FilterMode {
13991433
case FilterModeAll:

models/issue_test.go

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -253,6 +253,20 @@ func TestGetUserIssueStats(t *testing.T) {
253253
ClosedCount: 0,
254254
},
255255
},
256+
{
257+
UserIssueStatsOptions{
258+
UserID: 1,
259+
FilterMode: FilterModeCreate,
260+
IssueIDs: []int64{1},
261+
},
262+
IssueStats{
263+
YourRepositoriesCount: 0,
264+
AssignCount: 1,
265+
CreateCount: 1,
266+
OpenCount: 1,
267+
ClosedCount: 0,
268+
},
269+
},
256270
} {
257271
stats, err := GetUserIssueStats(test.Opts)
258272
if !assert.NoError(t, err) {
@@ -294,6 +308,36 @@ func TestIssue_SearchIssueIDsByKeyword(t *testing.T) {
294308
assert.EqualValues(t, []int64{1}, ids)
295309
}
296310

311+
func TestGetRepoIDsForIssuesOptions(t *testing.T) {
312+
assert.NoError(t, PrepareTestDatabase())
313+
user := AssertExistsAndLoadBean(t, &User{ID: 2}).(*User)
314+
for _, test := range []struct {
315+
Opts IssuesOptions
316+
ExpectedRepoIDs []int64
317+
}{
318+
{
319+
IssuesOptions{
320+
AssigneeID: 2,
321+
},
322+
[]int64{3},
323+
},
324+
{
325+
IssuesOptions{
326+
RepoIDs: []int64{1, 2},
327+
},
328+
[]int64{1, 2},
329+
},
330+
} {
331+
repoIDs, err := GetRepoIDsForIssuesOptions(&test.Opts, user)
332+
assert.NoError(t, err)
333+
if assert.Len(t, repoIDs, len(test.ExpectedRepoIDs)) {
334+
for i, repoID := range repoIDs {
335+
assert.EqualValues(t, test.ExpectedRepoIDs[i], repoID)
336+
}
337+
}
338+
}
339+
}
340+
297341
func testInsertIssue(t *testing.T, title, content string) {
298342
repo := AssertExistsAndLoadBean(t, &Repository{ID: 1}).(*Repository)
299343
user := AssertExistsAndLoadBean(t, &User{ID: 2}).(*User)

routers/user/home.go

Lines changed: 90 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ import (
1717
"code.gitea.io/gitea/models"
1818
"code.gitea.io/gitea/modules/base"
1919
"code.gitea.io/gitea/modules/context"
20+
issue_indexer "code.gitea.io/gitea/modules/indexer/issues"
2021
"code.gitea.io/gitea/modules/log"
2122
"code.gitea.io/gitea/modules/markup/markdown"
2223
"code.gitea.io/gitea/modules/setting"
@@ -449,7 +450,6 @@ func Issues(ctx *context.Context) {
449450
}
450451

451452
opts := &models.IssuesOptions{
452-
IsClosed: util.OptionalBoolOf(isShowClosed),
453453
IsPull: util.OptionalBoolOf(isPullList),
454454
SortType: sortType,
455455
}
@@ -465,10 +465,39 @@ func Issues(ctx *context.Context) {
465465
opts.MentionedID = ctxUser.ID
466466
}
467467

468-
counts, err := models.CountIssuesByRepo(opts)
469-
if err != nil {
470-
ctx.ServerError("CountIssuesByRepo", err)
471-
return
468+
var forceEmpty bool
469+
var issueIDsFromSearch []int64
470+
var keyword = strings.Trim(ctx.Query("q"), " ")
471+
472+
if len(keyword) > 0 {
473+
searchRepoIDs, err := models.GetRepoIDsForIssuesOptions(opts, ctxUser)
474+
if err != nil {
475+
ctx.ServerError("GetRepoIDsForIssuesOptions", err)
476+
return
477+
}
478+
issueIDsFromSearch, err = issue_indexer.SearchIssuesByKeyword(searchRepoIDs, keyword)
479+
if err != nil {
480+
ctx.ServerError("SearchIssuesByKeyword", err)
481+
return
482+
}
483+
if len(issueIDsFromSearch) > 0 {
484+
opts.IssueIDs = issueIDsFromSearch
485+
} else {
486+
forceEmpty = true
487+
}
488+
}
489+
490+
ctx.Data["Keyword"] = keyword
491+
492+
opts.IsClosed = util.OptionalBoolOf(isShowClosed)
493+
494+
var counts map[int64]int64
495+
if !forceEmpty {
496+
counts, err = models.CountIssuesByRepo(opts)
497+
if err != nil {
498+
ctx.ServerError("CountIssuesByRepo", err)
499+
return
500+
}
472501
}
473502

474503
opts.Page = page
@@ -488,10 +517,15 @@ func Issues(ctx *context.Context) {
488517
opts.RepoIDs = repoIDs
489518
}
490519

491-
issues, err := models.Issues(opts)
492-
if err != nil {
493-
ctx.ServerError("Issues", err)
494-
return
520+
var issues []*models.Issue
521+
if !forceEmpty {
522+
issues, err = models.Issues(opts)
523+
if err != nil {
524+
ctx.ServerError("Issues", err)
525+
return
526+
}
527+
} else {
528+
issues = []*models.Issue{}
495529
}
496530

497531
showReposMap := make(map[int64]*models.Repository, len(counts))
@@ -538,49 +572,78 @@ func Issues(ctx *context.Context) {
538572
}
539573
}
540574

541-
issueStatsOpts := models.UserIssueStatsOptions{
575+
userIssueStatsOpts := models.UserIssueStatsOptions{
542576
UserID: ctxUser.ID,
543577
UserRepoIDs: userRepoIDs,
544578
FilterMode: filterMode,
545579
IsPull: isPullList,
546580
IsClosed: isShowClosed,
547581
}
548582
if len(repoIDs) > 0 {
549-
issueStatsOpts.UserRepoIDs = repoIDs
583+
userIssueStatsOpts.UserRepoIDs = repoIDs
550584
}
551-
issueStats, err := models.GetUserIssueStats(issueStatsOpts)
585+
userIssueStats, err := models.GetUserIssueStats(userIssueStatsOpts)
552586
if err != nil {
553-
ctx.ServerError("GetUserIssueStats", err)
587+
ctx.ServerError("GetUserIssueStats User", err)
554588
return
555589
}
556590

557-
allIssueStats, err := models.GetUserIssueStats(models.UserIssueStatsOptions{
558-
UserID: ctxUser.ID,
559-
UserRepoIDs: userRepoIDs,
560-
FilterMode: filterMode,
561-
IsPull: isPullList,
562-
IsClosed: isShowClosed,
563-
})
564-
if err != nil {
565-
ctx.ServerError("GetUserIssueStats All", err)
566-
return
591+
var shownIssueStats *models.IssueStats
592+
if !forceEmpty {
593+
statsOpts := models.UserIssueStatsOptions{
594+
UserID: ctxUser.ID,
595+
UserRepoIDs: userRepoIDs,
596+
FilterMode: filterMode,
597+
IsPull: isPullList,
598+
IsClosed: isShowClosed,
599+
IssueIDs: issueIDsFromSearch,
600+
}
601+
if len(repoIDs) > 0 {
602+
statsOpts.RepoIDs = repoIDs
603+
}
604+
shownIssueStats, err = models.GetUserIssueStats(statsOpts)
605+
if err != nil {
606+
ctx.ServerError("GetUserIssueStats Shown", err)
607+
return
608+
}
609+
} else {
610+
shownIssueStats = &models.IssueStats{}
611+
}
612+
613+
var allIssueStats *models.IssueStats
614+
if !forceEmpty {
615+
allIssueStats, err = models.GetUserIssueStats(models.UserIssueStatsOptions{
616+
UserID: ctxUser.ID,
617+
UserRepoIDs: userRepoIDs,
618+
FilterMode: filterMode,
619+
IsPull: isPullList,
620+
IsClosed: isShowClosed,
621+
IssueIDs: issueIDsFromSearch,
622+
})
623+
if err != nil {
624+
ctx.ServerError("GetUserIssueStats All", err)
625+
return
626+
}
627+
} else {
628+
allIssueStats = &models.IssueStats{}
567629
}
568630

569631
var shownIssues int
570632
var totalIssues int
571633
if !isShowClosed {
572-
shownIssues = int(issueStats.OpenCount)
634+
shownIssues = int(shownIssueStats.OpenCount)
573635
totalIssues = int(allIssueStats.OpenCount)
574636
} else {
575-
shownIssues = int(issueStats.ClosedCount)
637+
shownIssues = int(shownIssueStats.ClosedCount)
576638
totalIssues = int(allIssueStats.ClosedCount)
577639
}
578640

579641
ctx.Data["Issues"] = issues
580642
ctx.Data["CommitStatus"] = commitStatus
581643
ctx.Data["Repos"] = showRepos
582644
ctx.Data["Counts"] = counts
583-
ctx.Data["IssueStats"] = issueStats
645+
ctx.Data["IssueStats"] = userIssueStats
646+
ctx.Data["ShownIssueStats"] = shownIssueStats
584647
ctx.Data["ViewType"] = viewType
585648
ctx.Data["SortType"] = sortType
586649
ctx.Data["RepoIDs"] = repoIDs
@@ -599,6 +662,7 @@ func Issues(ctx *context.Context) {
599662
ctx.Data["ReposParam"] = string(reposParam)
600663

601664
pager := context.NewPagination(shownIssues, setting.UI.IssuePagingNum, page, 5)
665+
pager.AddParam(ctx, "q", "Keyword")
602666
pager.AddParam(ctx, "type", "ViewType")
603667
pager.AddParam(ctx, "repos", "ReposParam")
604668
pager.AddParam(ctx, "sort", "SortType")

0 commit comments

Comments
 (0)