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