@@ -26,6 +26,7 @@ import (
26
26
"code.gitea.io/gitea/modules/container"
27
27
"code.gitea.io/gitea/modules/context"
28
28
issue_indexer "code.gitea.io/gitea/modules/indexer/issues"
29
+ "code.gitea.io/gitea/modules/json"
29
30
"code.gitea.io/gitea/modules/log"
30
31
"code.gitea.io/gitea/modules/markup"
31
32
"code.gitea.io/gitea/modules/markup/markdown"
@@ -349,6 +350,7 @@ func Pulls(ctx *context.Context) {
349
350
350
351
ctx .Data ["Title" ] = ctx .Tr ("pull_requests" )
351
352
ctx .Data ["PageIsPulls" ] = true
353
+ ctx .Data ["SingleRepoAction" ] = "pull"
352
354
buildIssueOverview (ctx , unit .TypePullRequests )
353
355
}
354
356
@@ -362,6 +364,7 @@ func Issues(ctx *context.Context) {
362
364
363
365
ctx .Data ["Title" ] = ctx .Tr ("issues" )
364
366
ctx .Data ["PageIsIssues" ] = true
367
+ ctx .Data ["SingleRepoAction" ] = "issue"
365
368
buildIssueOverview (ctx , unit .TypeIssues )
366
369
}
367
370
@@ -487,13 +490,6 @@ func buildIssueOverview(ctx *context.Context, unitType unit.Type) {
487
490
opts .RepoIDs = []int64 {0 }
488
491
}
489
492
}
490
- if ctx .Doer .ID == ctxUser .ID && filterMode != issues_model .FilterModeYourRepositories {
491
- // If the doer is the same as the context user, which means the doer is viewing his own dashboard,
492
- // it's not enough to show the repos that the doer owns or has been explicitly granted access to,
493
- // because the doer may create issues or be mentioned in any public repo.
494
- // So we need search issues in all public repos.
495
- opts .AllPublic = true
496
- }
497
493
498
494
switch filterMode {
499
495
case issues_model .FilterModeAll :
@@ -518,6 +514,14 @@ func buildIssueOverview(ctx *context.Context, unitType unit.Type) {
518
514
isShowClosed := ctx .FormString ("state" ) == "closed"
519
515
opts .IsClosed = util .OptionalBoolOf (isShowClosed )
520
516
517
+ // Filter repos and count issues in them. Count will be used later.
518
+ // USING NON-FINAL STATE OF opts FOR A QUERY.
519
+ issueCountByRepo , err := issue_indexer .CountIssuesByRepo (ctx , issue_indexer .ToSearchOptions (keyword , opts ))
520
+ if err != nil {
521
+ ctx .ServerError ("CountIssuesByRepo" , err )
522
+ return
523
+ }
524
+
521
525
// Make sure page number is at least 1. Will be posted to ctx.Data.
522
526
page := ctx .FormInt ("page" )
523
527
if page <= 1 {
@@ -542,6 +546,17 @@ func buildIssueOverview(ctx *context.Context, unitType unit.Type) {
542
546
}
543
547
opts .LabelIDs = labelIDs
544
548
549
+ // Parse ctx.FormString("repos") and remember matched repo IDs for later.
550
+ // Gets set when clicking filters on the issues overview page.
551
+ selectedRepoIDs := getRepoIDs (ctx .FormString ("repos" ))
552
+ // Remove repo IDs that are not accessible to the user.
553
+ selectedRepoIDs = slices .DeleteFunc (selectedRepoIDs , func (v int64 ) bool {
554
+ return ! accessibleRepos .Contains (v )
555
+ })
556
+ if len (selectedRepoIDs ) > 0 {
557
+ opts .RepoIDs = selectedRepoIDs
558
+ }
559
+
545
560
// ------------------------------
546
561
// Get issues as defined by opts.
547
562
// ------------------------------
@@ -562,6 +577,41 @@ func buildIssueOverview(ctx *context.Context, unitType unit.Type) {
562
577
}
563
578
}
564
579
580
+ // ----------------------------------
581
+ // Add repository pointers to Issues.
582
+ // ----------------------------------
583
+
584
+ // Remove repositories that should not be shown,
585
+ // which are repositories that have no issues and are not selected by the user.
586
+ selectedRepos := container .SetOf (selectedRepoIDs ... )
587
+ for k , v := range issueCountByRepo {
588
+ if v == 0 && ! selectedRepos .Contains (k ) {
589
+ delete (issueCountByRepo , k )
590
+ }
591
+ }
592
+
593
+ // showReposMap maps repository IDs to their Repository pointers.
594
+ showReposMap , err := loadRepoByIDs (ctx , ctxUser , issueCountByRepo , unitType )
595
+ if err != nil {
596
+ if repo_model .IsErrRepoNotExist (err ) {
597
+ ctx .NotFound ("GetRepositoryByID" , err )
598
+ return
599
+ }
600
+ ctx .ServerError ("loadRepoByIDs" , err )
601
+ return
602
+ }
603
+
604
+ // a RepositoryList
605
+ showRepos := repo_model .RepositoryListOfMap (showReposMap )
606
+ sort .Sort (showRepos )
607
+
608
+ // maps pull request IDs to their CommitStatus. Will be posted to ctx.Data.
609
+ for _ , issue := range issues {
610
+ if issue .Repo == nil {
611
+ issue .Repo = showReposMap [issue .RepoID ]
612
+ }
613
+ }
614
+
565
615
commitStatuses , lastStatus , err := pull_service .GetIssuesAllCommitStatus (ctx , issues )
566
616
if err != nil {
567
617
ctx .ServerError ("GetIssuesLastCommitStatus" , err )
@@ -571,7 +621,7 @@ func buildIssueOverview(ctx *context.Context, unitType unit.Type) {
571
621
// -------------------------------
572
622
// Fill stats to post to ctx.Data.
573
623
// -------------------------------
574
- issueStats , err := getUserIssueStats (ctx , ctxUser , filterMode , issue_indexer .ToSearchOptions (keyword , opts ))
624
+ issueStats , err := getUserIssueStats (ctx , filterMode , issue_indexer .ToSearchOptions (keyword , opts ), ctx . Doer . ID )
575
625
if err != nil {
576
626
ctx .ServerError ("getUserIssueStats" , err )
577
627
return
@@ -584,6 +634,25 @@ func buildIssueOverview(ctx *context.Context, unitType unit.Type) {
584
634
} else {
585
635
shownIssues = int (issueStats .ClosedCount )
586
636
}
637
+ if len (opts .RepoIDs ) != 0 {
638
+ shownIssues = 0
639
+ for _ , repoID := range opts .RepoIDs {
640
+ shownIssues += int (issueCountByRepo [repoID ])
641
+ }
642
+ }
643
+
644
+ var allIssueCount int64
645
+ for _ , issueCount := range issueCountByRepo {
646
+ allIssueCount += issueCount
647
+ }
648
+ ctx .Data ["TotalIssueCount" ] = allIssueCount
649
+
650
+ if len (opts .RepoIDs ) == 1 {
651
+ repo := showReposMap [opts .RepoIDs [0 ]]
652
+ if repo != nil {
653
+ ctx .Data ["SingleRepoLink" ] = repo .Link ()
654
+ }
655
+ }
587
656
588
657
ctx .Data ["IsShowClosed" ] = isShowClosed
589
658
@@ -620,9 +689,12 @@ func buildIssueOverview(ctx *context.Context, unitType unit.Type) {
620
689
}
621
690
ctx .Data ["CommitLastStatus" ] = lastStatus
622
691
ctx .Data ["CommitStatuses" ] = commitStatuses
692
+ ctx .Data ["Repos" ] = showRepos
693
+ ctx .Data ["Counts" ] = issueCountByRepo
623
694
ctx .Data ["IssueStats" ] = issueStats
624
695
ctx .Data ["ViewType" ] = viewType
625
696
ctx .Data ["SortType" ] = sortType
697
+ ctx .Data ["RepoIDs" ] = selectedRepoIDs
626
698
ctx .Data ["IsShowClosed" ] = isShowClosed
627
699
ctx .Data ["SelectLabels" ] = selectedLabels
628
700
@@ -632,9 +704,15 @@ func buildIssueOverview(ctx *context.Context, unitType unit.Type) {
632
704
ctx .Data ["State" ] = "open"
633
705
}
634
706
707
+ // Convert []int64 to string
708
+ reposParam , _ := json .Marshal (opts .RepoIDs )
709
+
710
+ ctx .Data ["ReposParam" ] = string (reposParam )
711
+
635
712
pager := context .NewPagination (shownIssues , setting .UI .IssuePagingNum , page , 5 )
636
713
pager .AddParam (ctx , "q" , "Keyword" )
637
714
pager .AddParam (ctx , "type" , "ViewType" )
715
+ pager .AddParam (ctx , "repos" , "ReposParam" )
638
716
pager .AddParam (ctx , "sort" , "SortType" )
639
717
pager .AddParam (ctx , "state" , "State" )
640
718
pager .AddParam (ctx , "labels" , "SelectLabels" )
@@ -645,6 +723,55 @@ func buildIssueOverview(ctx *context.Context, unitType unit.Type) {
645
723
ctx .HTML (http .StatusOK , tplIssues )
646
724
}
647
725
726
+ func getRepoIDs (reposQuery string ) []int64 {
727
+ if len (reposQuery ) == 0 || reposQuery == "[]" {
728
+ return []int64 {}
729
+ }
730
+ if ! issueReposQueryPattern .MatchString (reposQuery ) {
731
+ log .Warn ("issueReposQueryPattern does not match query: %q" , reposQuery )
732
+ return []int64 {}
733
+ }
734
+
735
+ var repoIDs []int64
736
+ // remove "[" and "]" from string
737
+ reposQuery = reposQuery [1 : len (reposQuery )- 1 ]
738
+ // for each ID (delimiter ",") add to int to repoIDs
739
+ for _ , rID := range strings .Split (reposQuery , "," ) {
740
+ // Ensure nonempty string entries
741
+ if rID != "" && rID != "0" {
742
+ rIDint64 , err := strconv .ParseInt (rID , 10 , 64 )
743
+ if err == nil {
744
+ repoIDs = append (repoIDs , rIDint64 )
745
+ }
746
+ }
747
+ }
748
+
749
+ return repoIDs
750
+ }
751
+
752
+ func loadRepoByIDs (ctx * context.Context , ctxUser * user_model.User , issueCountByRepo map [int64 ]int64 , unitType unit.Type ) (map [int64 ]* repo_model.Repository , error ) {
753
+ totalRes := make (map [int64 ]* repo_model.Repository , len (issueCountByRepo ))
754
+ repoIDs := make ([]int64 , 0 , 500 )
755
+ for id := range issueCountByRepo {
756
+ if id <= 0 {
757
+ continue
758
+ }
759
+ repoIDs = append (repoIDs , id )
760
+ if len (repoIDs ) == 500 {
761
+ if err := repo_model .FindReposMapByIDs (ctx , repoIDs , totalRes ); err != nil {
762
+ return nil , err
763
+ }
764
+ repoIDs = repoIDs [:0 ]
765
+ }
766
+ }
767
+ if len (repoIDs ) > 0 {
768
+ if err := repo_model .FindReposMapByIDs (ctx , repoIDs , totalRes ); err != nil {
769
+ return nil , err
770
+ }
771
+ }
772
+ return totalRes , nil
773
+ }
774
+
648
775
// ShowSSHKeys output all the ssh keys of user by uid
649
776
func ShowSSHKeys (ctx * context.Context ) {
650
777
keys , err := db .Find [asymkey_model.PublicKey ](ctx , asymkey_model.FindPublicKeyOptions {
@@ -761,15 +888,8 @@ func UsernameSubRoute(ctx *context.Context) {
761
888
}
762
889
}
763
890
764
- func getUserIssueStats (ctx * context.Context , ctxUser * user_model.User , filterMode int , opts * issue_indexer.SearchOptions ) (* issues_model.IssueStats , error ) {
765
- doerID := ctx .Doer .ID
766
-
891
+ func getUserIssueStats (ctx * context.Context , filterMode int , opts * issue_indexer.SearchOptions , doerID int64 ) (* issues_model.IssueStats , error ) {
767
892
opts = opts .Copy (func (o * issue_indexer.SearchOptions ) {
768
- // If the doer is the same as the context user, which means the doer is viewing his own dashboard,
769
- // it's not enough to show the repos that the doer owns or has been explicitly granted access to,
770
- // because the doer may create issues or be mentioned in any public repo.
771
- // So we need search issues in all public repos.
772
- o .AllPublic = doerID == ctxUser .ID
773
893
o .AssigneeID = nil
774
894
o .PosterID = nil
775
895
o .MentionID = nil
@@ -785,10 +905,7 @@ func getUserIssueStats(ctx *context.Context, ctxUser *user_model.User, filterMod
785
905
{
786
906
openClosedOpts := opts .Copy ()
787
907
switch filterMode {
788
- case issues_model .FilterModeAll :
789
- // no-op
790
- case issues_model .FilterModeYourRepositories :
791
- openClosedOpts .AllPublic = false
908
+ case issues_model .FilterModeAll , issues_model .FilterModeYourRepositories :
792
909
case issues_model .FilterModeAssign :
793
910
openClosedOpts .AssigneeID = & doerID
794
911
case issues_model .FilterModeCreate :
@@ -812,7 +929,7 @@ func getUserIssueStats(ctx *context.Context, ctxUser *user_model.User, filterMod
812
929
}
813
930
}
814
931
815
- ret .YourRepositoriesCount , err = issue_indexer .CountIssues (ctx , opts . Copy ( func ( o * issue_indexer. SearchOptions ) { o . AllPublic = false }) )
932
+ ret .YourRepositoriesCount , err = issue_indexer .CountIssues (ctx , opts )
816
933
if err != nil {
817
934
return nil , err
818
935
}
0 commit comments