Skip to content

Commit 5db21ce

Browse files
authored
Fix counting and filtering on the dashboard page for issues (#26657)
This PR has multiple parts, and I didn't split them because it's not easy to test them separately since they are all about the dashboard page for issues. 1. Support counting issues via indexer to fix #26361 2. Fix repo selection so it also fixes #26653 3. Keep keywords in filter links. The first two are regressions of #26012. After: https://github.com/go-gitea/gitea/assets/9418365/71dfea7e-d9e2-42b6-851a-cc081435c946 Thanks to @CaiCandong for helping with some tests.
1 parent 3b91b2d commit 5db21ce

File tree

5 files changed

+187
-110
lines changed

5 files changed

+187
-110
lines changed

models/repo/repo_list.go

+23-13
Original file line numberDiff line numberDiff line change
@@ -130,6 +130,10 @@ type SearchRepoOptions struct {
130130
// True -> include just collaborative
131131
// False -> include just non-collaborative
132132
Collaborate util.OptionalBool
133+
// What type of unit the user can be collaborative in,
134+
// it is ignored if Collaborate is False.
135+
// TypeInvalid means any unit type.
136+
UnitType unit.Type
133137
// None -> include forks AND non-forks
134138
// True -> include just forks
135139
// False -> include just non-forks
@@ -382,19 +386,25 @@ func SearchRepositoryCondition(opts *SearchRepoOptions) builder.Cond {
382386

383387
if opts.Collaborate != util.OptionalBoolFalse {
384388
// A Collaboration is:
385-
collaborateCond := builder.And(
386-
// 1. Repository we don't own
387-
builder.Neq{"owner_id": opts.OwnerID},
388-
// 2. But we can see because of:
389-
builder.Or(
390-
// A. We have unit independent access
391-
UserAccessRepoCond("`repository`.id", opts.OwnerID),
392-
// B. We are in a team for
393-
UserOrgTeamRepoCond("`repository`.id", opts.OwnerID),
394-
// C. Public repositories in organizations that we are member of
395-
userOrgPublicRepoCondPrivate(opts.OwnerID),
396-
),
397-
)
389+
390+
collaborateCond := builder.NewCond()
391+
// 1. Repository we don't own
392+
collaborateCond = collaborateCond.And(builder.Neq{"owner_id": opts.OwnerID})
393+
// 2. But we can see because of:
394+
{
395+
userAccessCond := builder.NewCond()
396+
// A. We have unit independent access
397+
userAccessCond = userAccessCond.Or(UserAccessRepoCond("`repository`.id", opts.OwnerID))
398+
// B. We are in a team for
399+
if opts.UnitType == unit.TypeInvalid {
400+
userAccessCond = userAccessCond.Or(UserOrgTeamRepoCond("`repository`.id", opts.OwnerID))
401+
} else {
402+
userAccessCond = userAccessCond.Or(userOrgTeamUnitRepoCond("`repository`.id", opts.OwnerID, opts.UnitType))
403+
}
404+
// C. Public repositories in organizations that we are member of
405+
userAccessCond = userAccessCond.Or(userOrgPublicRepoCondPrivate(opts.OwnerID))
406+
collaborateCond = collaborateCond.And(userAccessCond)
407+
}
398408
if !opts.Private {
399409
collaborateCond = collaborateCond.And(builder.Expr("owner_id NOT IN (SELECT org_id FROM org_user WHERE org_user.uid = ? AND org_user.is_public = ?)", opts.OwnerID, false))
400410
}

modules/indexer/issues/indexer.go

+38-3
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import (
1313

1414
db_model "code.gitea.io/gitea/models/db"
1515
repo_model "code.gitea.io/gitea/models/repo"
16+
"code.gitea.io/gitea/modules/container"
1617
"code.gitea.io/gitea/modules/graceful"
1718
"code.gitea.io/gitea/modules/indexer/issues/bleve"
1819
"code.gitea.io/gitea/modules/indexer/issues/db"
@@ -277,7 +278,7 @@ func IsAvailable(ctx context.Context) bool {
277278
}
278279

279280
// SearchOptions indicates the options for searching issues
280-
type SearchOptions internal.SearchOptions
281+
type SearchOptions = internal.SearchOptions
281282

282283
const (
283284
SortByCreatedDesc = internal.SortByCreatedDesc
@@ -291,7 +292,6 @@ const (
291292
)
292293

293294
// SearchIssues search issues by options.
294-
// It returns issue ids and a bool value indicates if the result is imprecise.
295295
func SearchIssues(ctx context.Context, opts *SearchOptions) ([]int64, int64, error) {
296296
indexer := *globalIndexer.Load()
297297

@@ -305,7 +305,7 @@ func SearchIssues(ctx context.Context, opts *SearchOptions) ([]int64, int64, err
305305
indexer = db.NewIndexer()
306306
}
307307

308-
result, err := indexer.Search(ctx, (*internal.SearchOptions)(opts))
308+
result, err := indexer.Search(ctx, opts)
309309
if err != nil {
310310
return nil, 0, err
311311
}
@@ -317,3 +317,38 @@ func SearchIssues(ctx context.Context, opts *SearchOptions) ([]int64, int64, err
317317

318318
return ret, result.Total, nil
319319
}
320+
321+
// CountIssues counts issues by options. It is a shortcut of SearchIssues(ctx, opts) but only returns the total count.
322+
func CountIssues(ctx context.Context, opts *SearchOptions) (int64, error) {
323+
opts = opts.Copy(func(options *SearchOptions) { opts.Paginator = &db_model.ListOptions{PageSize: 0} })
324+
325+
_, total, err := SearchIssues(ctx, opts)
326+
return total, err
327+
}
328+
329+
// CountIssuesByRepo counts issues by options and group by repo id.
330+
// It's not a complete implementation, since it requires the caller should provide the repo ids.
331+
// That means opts.RepoIDs must be specified, and opts.AllPublic must be false.
332+
// It's good enough for the current usage, and it can be improved if needed.
333+
// TODO: use "group by" of the indexer engines to implement it.
334+
func CountIssuesByRepo(ctx context.Context, opts *SearchOptions) (map[int64]int64, error) {
335+
if len(opts.RepoIDs) == 0 {
336+
return nil, fmt.Errorf("opts.RepoIDs must be specified")
337+
}
338+
if opts.AllPublic {
339+
return nil, fmt.Errorf("opts.AllPublic must be false")
340+
}
341+
342+
repoIDs := container.SetOf(opts.RepoIDs...).Values()
343+
ret := make(map[int64]int64, len(repoIDs))
344+
// TODO: it could be faster if do it in parallel for some indexer engines. Improve it if users report it's slow.
345+
for _, repoID := range repoIDs {
346+
count, err := CountIssues(ctx, opts.Copy(func(o *internal.SearchOptions) { o.RepoIDs = []int64{repoID} }))
347+
if err != nil {
348+
return nil, err
349+
}
350+
ret[repoID] = count
351+
}
352+
353+
return ret, nil
354+
}

modules/indexer/issues/internal/model.go

+13
Original file line numberDiff line numberDiff line change
@@ -109,6 +109,19 @@ type SearchOptions struct {
109109
SortBy SortBy // sort by field
110110
}
111111

112+
// Copy returns a copy of the options.
113+
// Be careful, it's not a deep copy, so `SearchOptions.RepoIDs = {...}` is OK while `SearchOptions.RepoIDs[0] = ...` is not.
114+
func (o *SearchOptions) Copy(edit ...func(options *SearchOptions)) *SearchOptions {
115+
if o == nil {
116+
return nil
117+
}
118+
v := *o
119+
for _, e := range edit {
120+
e(&v)
121+
}
122+
return &v
123+
}
124+
112125
type SortBy string
113126

114127
const (

0 commit comments

Comments
 (0)