@@ -25,6 +25,7 @@ import (
2525 "code.gitea.io/gitea/modules/base"
2626 "code.gitea.io/gitea/modules/container"
2727 issue_indexer "code.gitea.io/gitea/modules/indexer/issues"
28+ "code.gitea.io/gitea/modules/json"
2829 "code.gitea.io/gitea/modules/log"
2930 "code.gitea.io/gitea/modules/markup"
3031 "code.gitea.io/gitea/modules/markup/markdown"
@@ -348,6 +349,7 @@ func Pulls(ctx *context.Context) {
348349
349350 ctx .Data ["Title" ] = ctx .Tr ("pull_requests" )
350351 ctx .Data ["PageIsPulls" ] = true
352+ ctx .Data ["SingleRepoAction" ] = "pull"
351353 buildIssueOverview (ctx , unit .TypePullRequests )
352354}
353355
@@ -361,6 +363,7 @@ func Issues(ctx *context.Context) {
361363
362364 ctx .Data ["Title" ] = ctx .Tr ("issues" )
363365 ctx .Data ["PageIsIssues" ] = true
366+ ctx .Data ["SingleRepoAction" ] = "issue"
364367 buildIssueOverview (ctx , unit .TypeIssues )
365368}
366369
@@ -486,13 +489,6 @@ func buildIssueOverview(ctx *context.Context, unitType unit.Type) {
486489 opts .RepoIDs = []int64 {0 }
487490 }
488491 }
489- if ctx .Doer .ID == ctxUser .ID && filterMode != issues_model .FilterModeYourRepositories {
490- // If the doer is the same as the context user, which means the doer is viewing his own dashboard,
491- // it's not enough to show the repos that the doer owns or has been explicitly granted access to,
492- // because the doer may create issues or be mentioned in any public repo.
493- // So we need search issues in all public repos.
494- opts .AllPublic = true
495- }
496492
497493 switch filterMode {
498494 case issues_model .FilterModeAll :
@@ -517,6 +513,14 @@ func buildIssueOverview(ctx *context.Context, unitType unit.Type) {
517513 isShowClosed := ctx .FormString ("state" ) == "closed"
518514 opts .IsClosed = optional .Some (isShowClosed )
519515
516+ // Filter repos and count issues in them. Count will be used later.
517+ // USING NON-FINAL STATE OF opts FOR A QUERY.
518+ issueCountByRepo , err := issue_indexer .CountIssuesByRepo (ctx , issue_indexer .ToSearchOptions (keyword , opts ))
519+ if err != nil {
520+ ctx .ServerError ("CountIssuesByRepo" , err )
521+ return
522+ }
523+
520524 // Make sure page number is at least 1. Will be posted to ctx.Data.
521525 page := ctx .FormInt ("page" )
522526 if page <= 1 {
@@ -541,6 +545,17 @@ func buildIssueOverview(ctx *context.Context, unitType unit.Type) {
541545 }
542546 opts .LabelIDs = labelIDs
543547
548+ // Parse ctx.FormString("repos") and remember matched repo IDs for later.
549+ // Gets set when clicking filters on the issues overview page.
550+ selectedRepoIDs := getRepoIDs (ctx .FormString ("repos" ))
551+ // Remove repo IDs that are not accessible to the user.
552+ selectedRepoIDs = slices .DeleteFunc (selectedRepoIDs , func (v int64 ) bool {
553+ return ! accessibleRepos .Contains (v )
554+ })
555+ if len (selectedRepoIDs ) > 0 {
556+ opts .RepoIDs = selectedRepoIDs
557+ }
558+
544559 // ------------------------------
545560 // Get issues as defined by opts.
546561 // ------------------------------
@@ -561,6 +576,41 @@ func buildIssueOverview(ctx *context.Context, unitType unit.Type) {
561576 }
562577 }
563578
579+ // ----------------------------------
580+ // Add repository pointers to Issues.
581+ // ----------------------------------
582+
583+ // Remove repositories that should not be shown,
584+ // which are repositories that have no issues and are not selected by the user.
585+ selectedRepos := container .SetOf (selectedRepoIDs ... )
586+ for k , v := range issueCountByRepo {
587+ if v == 0 && ! selectedRepos .Contains (k ) {
588+ delete (issueCountByRepo , k )
589+ }
590+ }
591+
592+ // showReposMap maps repository IDs to their Repository pointers.
593+ showReposMap , err := loadRepoByIDs (ctx , ctxUser , issueCountByRepo , unitType )
594+ if err != nil {
595+ if repo_model .IsErrRepoNotExist (err ) {
596+ ctx .NotFound ("GetRepositoryByID" , err )
597+ return
598+ }
599+ ctx .ServerError ("loadRepoByIDs" , err )
600+ return
601+ }
602+
603+ // a RepositoryList
604+ showRepos := repo_model .RepositoryListOfMap (showReposMap )
605+ sort .Sort (showRepos )
606+
607+ // maps pull request IDs to their CommitStatus. Will be posted to ctx.Data.
608+ for _ , issue := range issues {
609+ if issue .Repo == nil {
610+ issue .Repo = showReposMap [issue .RepoID ]
611+ }
612+ }
613+
564614 commitStatuses , lastStatus , err := pull_service .GetIssuesAllCommitStatus (ctx , issues )
565615 if err != nil {
566616 ctx .ServerError ("GetIssuesLastCommitStatus" , err )
@@ -570,7 +620,7 @@ func buildIssueOverview(ctx *context.Context, unitType unit.Type) {
570620 // -------------------------------
571621 // Fill stats to post to ctx.Data.
572622 // -------------------------------
573- issueStats , err := getUserIssueStats (ctx , ctxUser , filterMode , issue_indexer .ToSearchOptions (keyword , opts ))
623+ issueStats , err := getUserIssueStats (ctx , filterMode , issue_indexer .ToSearchOptions (keyword , opts ), ctx . Doer . ID )
574624 if err != nil {
575625 ctx .ServerError ("getUserIssueStats" , err )
576626 return
@@ -583,6 +633,25 @@ func buildIssueOverview(ctx *context.Context, unitType unit.Type) {
583633 } else {
584634 shownIssues = int (issueStats .ClosedCount )
585635 }
636+ if len (opts .RepoIDs ) != 0 {
637+ shownIssues = 0
638+ for _ , repoID := range opts .RepoIDs {
639+ shownIssues += int (issueCountByRepo [repoID ])
640+ }
641+ }
642+
643+ var allIssueCount int64
644+ for _ , issueCount := range issueCountByRepo {
645+ allIssueCount += issueCount
646+ }
647+ ctx .Data ["TotalIssueCount" ] = allIssueCount
648+
649+ if len (opts .RepoIDs ) == 1 {
650+ repo := showReposMap [opts .RepoIDs [0 ]]
651+ if repo != nil {
652+ ctx .Data ["SingleRepoLink" ] = repo .Link ()
653+ }
654+ }
586655
587656 ctx .Data ["IsShowClosed" ] = isShowClosed
588657
@@ -619,9 +688,12 @@ func buildIssueOverview(ctx *context.Context, unitType unit.Type) {
619688 }
620689 ctx .Data ["CommitLastStatus" ] = lastStatus
621690 ctx .Data ["CommitStatuses" ] = commitStatuses
691+ ctx .Data ["Repos" ] = showRepos
692+ ctx .Data ["Counts" ] = issueCountByRepo
622693 ctx .Data ["IssueStats" ] = issueStats
623694 ctx .Data ["ViewType" ] = viewType
624695 ctx .Data ["SortType" ] = sortType
696+ ctx .Data ["RepoIDs" ] = selectedRepoIDs
625697 ctx .Data ["IsShowClosed" ] = isShowClosed
626698 ctx .Data ["SelectLabels" ] = selectedLabels
627699
@@ -631,9 +703,15 @@ func buildIssueOverview(ctx *context.Context, unitType unit.Type) {
631703 ctx .Data ["State" ] = "open"
632704 }
633705
706+ // Convert []int64 to string
707+ reposParam , _ := json .Marshal (opts .RepoIDs )
708+
709+ ctx .Data ["ReposParam" ] = string (reposParam )
710+
634711 pager := context .NewPagination (shownIssues , setting .UI .IssuePagingNum , page , 5 )
635712 pager .AddParam (ctx , "q" , "Keyword" )
636713 pager .AddParam (ctx , "type" , "ViewType" )
714+ pager .AddParam (ctx , "repos" , "ReposParam" )
637715 pager .AddParam (ctx , "sort" , "SortType" )
638716 pager .AddParam (ctx , "state" , "State" )
639717 pager .AddParam (ctx , "labels" , "SelectLabels" )
@@ -644,6 +722,55 @@ func buildIssueOverview(ctx *context.Context, unitType unit.Type) {
644722 ctx .HTML (http .StatusOK , tplIssues )
645723}
646724
725+ func getRepoIDs (reposQuery string ) []int64 {
726+ if len (reposQuery ) == 0 || reposQuery == "[]" {
727+ return []int64 {}
728+ }
729+ if ! issueReposQueryPattern .MatchString (reposQuery ) {
730+ log .Warn ("issueReposQueryPattern does not match query: %q" , reposQuery )
731+ return []int64 {}
732+ }
733+
734+ var repoIDs []int64
735+ // remove "[" and "]" from string
736+ reposQuery = reposQuery [1 : len (reposQuery )- 1 ]
737+ // for each ID (delimiter ",") add to int to repoIDs
738+ for _ , rID := range strings .Split (reposQuery , "," ) {
739+ // Ensure nonempty string entries
740+ if rID != "" && rID != "0" {
741+ rIDint64 , err := strconv .ParseInt (rID , 10 , 64 )
742+ if err == nil {
743+ repoIDs = append (repoIDs , rIDint64 )
744+ }
745+ }
746+ }
747+
748+ return repoIDs
749+ }
750+
751+ func loadRepoByIDs (ctx * context.Context , ctxUser * user_model.User , issueCountByRepo map [int64 ]int64 , unitType unit.Type ) (map [int64 ]* repo_model.Repository , error ) {
752+ totalRes := make (map [int64 ]* repo_model.Repository , len (issueCountByRepo ))
753+ repoIDs := make ([]int64 , 0 , 500 )
754+ for id := range issueCountByRepo {
755+ if id <= 0 {
756+ continue
757+ }
758+ repoIDs = append (repoIDs , id )
759+ if len (repoIDs ) == 500 {
760+ if err := repo_model .FindReposMapByIDs (ctx , repoIDs , totalRes ); err != nil {
761+ return nil , err
762+ }
763+ repoIDs = repoIDs [:0 ]
764+ }
765+ }
766+ if len (repoIDs ) > 0 {
767+ if err := repo_model .FindReposMapByIDs (ctx , repoIDs , totalRes ); err != nil {
768+ return nil , err
769+ }
770+ }
771+ return totalRes , nil
772+ }
773+
647774// ShowSSHKeys output all the ssh keys of user by uid
648775func ShowSSHKeys (ctx * context.Context ) {
649776 keys , err := db .Find [asymkey_model.PublicKey ](ctx , asymkey_model.FindPublicKeyOptions {
@@ -760,15 +887,8 @@ func UsernameSubRoute(ctx *context.Context) {
760887 }
761888}
762889
763- func getUserIssueStats (ctx * context.Context , ctxUser * user_model.User , filterMode int , opts * issue_indexer.SearchOptions ) (* issues_model.IssueStats , error ) {
764- doerID := ctx .Doer .ID
765-
890+ func getUserIssueStats (ctx * context.Context , filterMode int , opts * issue_indexer.SearchOptions , doerID int64 ) (* issues_model.IssueStats , error ) {
766891 opts = opts .Copy (func (o * issue_indexer.SearchOptions ) {
767- // If the doer is the same as the context user, which means the doer is viewing his own dashboard,
768- // it's not enough to show the repos that the doer owns or has been explicitly granted access to,
769- // because the doer may create issues or be mentioned in any public repo.
770- // So we need search issues in all public repos.
771- o .AllPublic = doerID == ctxUser .ID
772892 o .AssigneeID = nil
773893 o .PosterID = nil
774894 o .MentionID = nil
@@ -784,10 +904,7 @@ func getUserIssueStats(ctx *context.Context, ctxUser *user_model.User, filterMod
784904 {
785905 openClosedOpts := opts .Copy ()
786906 switch filterMode {
787- case issues_model .FilterModeAll :
788- // no-op
789- case issues_model .FilterModeYourRepositories :
790- openClosedOpts .AllPublic = false
907+ case issues_model .FilterModeAll , issues_model .FilterModeYourRepositories :
791908 case issues_model .FilterModeAssign :
792909 openClosedOpts .AssigneeID = & doerID
793910 case issues_model .FilterModeCreate :
@@ -811,7 +928,7 @@ func getUserIssueStats(ctx *context.Context, ctxUser *user_model.User, filterMod
811928 }
812929 }
813930
814- ret .YourRepositoriesCount , err = issue_indexer .CountIssues (ctx , opts . Copy ( func ( o * issue_indexer. SearchOptions ) { o . AllPublic = false }) )
931+ ret .YourRepositoriesCount , err = issue_indexer .CountIssues (ctx , opts )
815932 if err != nil {
816933 return nil , err
817934 }
0 commit comments