diff --git a/models/issue.go b/models/issue.go index b05c8cccdd..f0040fbbc1 100644 --- a/models/issue.go +++ b/models/issue.go @@ -1186,6 +1186,9 @@ type IssuesOptions struct { // prioritize issues from this repo PriorityRepoID int64 IsArchived util.OptionalBool + Org *Organization // issues permission scope + Team *Team // issues permission scope + User *user_model.User // issues permission scope } // sortIssuesSession sort an issues-related session based on the provided @@ -1337,6 +1340,44 @@ func (opts *IssuesOptions) setupSession(sess *xorm.Session) { From("milestone"). Where(builder.In("name", opts.IncludeMilestones))) } + + if opts.User != nil { + sess.And( + issuePullAccessibleRepoCond("issue.repo_id", opts.User.ID, opts.Org, opts.Team, opts.IsPull.IsTrue()), + ) + } +} + +// issuePullAccessibleRepoCond userID must not be zero, this condition require join repository table +func issuePullAccessibleRepoCond(repoIDstr string, userID int64, org *Organization, team *Team, isPull bool) builder.Cond { + var cond = builder.NewCond() + var unitType = unit.TypeIssues + if isPull { + unitType = unit.TypePullRequests + } + if org != nil { + if team != nil { + cond = cond.And(teamUnitsRepoCond(repoIDstr, userID, org.ID, team.ID, unitType)) // special team member repos + } else { + cond = cond.And( + builder.Or( + userOrgUnitRepoCond(repoIDstr, userID, org.ID, unitType), // team member repos + userOrgPublicUnitRepoCond(userID, org.ID), // user org public non-member repos, TODO: check repo has issues + ), + ) + } + } else { + cond = cond.And( + builder.Or( + userOwnedRepoCond(userID), // owned repos + userCollaborationRepoCond(repoIDstr, userID), // collaboration repos + userAssignedRepoCond(repoIDstr, userID), // user has been assigned accessible public repos + userMentionedRepoCond(repoIDstr, userID), // user has been mentioned accessible public repos + userCreateIssueRepoCond(repoIDstr, userID, isPull), // user has created issue/pr accessible public repos + ), + ) + } + return cond } func applyReposCondition(sess *xorm.Session, repoIDs []int64) *xorm.Session { @@ -1646,15 +1687,16 @@ func getIssueStatsChunk(opts *IssueStatsOptions, issueIDs []int64) (*IssueStats, // UserIssueStatsOptions contains parameters accepted by GetUserIssueStats. type UserIssueStatsOptions struct { - UserID int64 - RepoIDs []int64 - UserRepoIDs []int64 - FilterMode int - IsPull bool - IsClosed bool - IssueIDs []int64 - IsArchived util.OptionalBool - LabelIDs []int64 + UserID int64 + RepoIDs []int64 + FilterMode int + IsPull bool + IsClosed bool + IssueIDs []int64 + IsArchived util.OptionalBool + LabelIDs []int64 + Org *Organization + Team *Team } // GetUserIssueStats returns issue statistic information for dashboard by given conditions. @@ -1671,28 +1713,34 @@ func GetUserIssueStats(opts UserIssueStatsOptions) (*IssueStats, error) { cond = cond.And(builder.In("issue.id", opts.IssueIDs)) } + if opts.UserID > 0 { + cond = cond.And(issuePullAccessibleRepoCond("issue.repo_id", opts.UserID, opts.Org, opts.Team, opts.IsPull)) + } + sess := func(cond builder.Cond) *xorm.Session { s := db.GetEngine(db.DefaultContext).Where(cond) if len(opts.LabelIDs) > 0 { s.Join("INNER", "issue_label", "issue_label.issue_id = issue.id"). In("issue_label.label_id", opts.LabelIDs) } - if opts.IsArchived != util.OptionalBoolNone { - s.Join("INNER", "repository", "issue.repo_id = repository.id"). - And(builder.Eq{"repository.is_archived": opts.IsArchived.IsTrue()}) + if opts.UserID > 0 || opts.IsArchived != util.OptionalBoolNone { + s.Join("INNER", "repository", "issue.repo_id = repository.id") + if opts.IsArchived != util.OptionalBoolNone { + s.And(builder.Eq{"repository.is_archived": opts.IsArchived.IsTrue()}) + } } return s } switch opts.FilterMode { case FilterModeAll: - stats.OpenCount, err = applyReposCondition(sess(cond), opts.UserRepoIDs). + stats.OpenCount, err = sess(cond). And("issue.is_closed = ?", false). Count(new(Issue)) if err != nil { return nil, err } - stats.ClosedCount, err = applyReposCondition(sess(cond), opts.UserRepoIDs). + stats.ClosedCount, err = sess(cond). And("issue.is_closed = ?", true). Count(new(Issue)) if err != nil { @@ -1768,7 +1816,7 @@ func GetUserIssueStats(opts UserIssueStatsOptions) (*IssueStats, error) { return nil, err } - stats.YourRepositoriesCount, err = applyReposCondition(sess(cond), opts.UserRepoIDs).Count(new(Issue)) + stats.YourRepositoriesCount, err = sess(cond).Count(new(Issue)) if err != nil { return nil, err } diff --git a/models/issue_list.go b/models/issue_list.go index 5d8a9f6921..b516e7336e 100644 --- a/models/issue_list.go +++ b/models/issue_list.go @@ -25,6 +25,9 @@ const ( func (issues IssueList) getRepoIDs() []int64 { repoIDs := make(map[int64]struct{}, len(issues)) for _, issue := range issues { + if issue.Repo != nil { + continue + } if _, ok := repoIDs[issue.RepoID]; !ok { repoIDs[issue.RepoID] = struct{}{} } @@ -56,8 +59,12 @@ func (issues IssueList) loadRepositories(e db.Engine) ([]*repo_model.Repository, } for _, issue := range issues { - issue.Repo = repoMaps[issue.RepoID] - if issue.PullRequest != nil { + if issue.Repo == nil { + issue.Repo = repoMaps[issue.RepoID] + } else { + repoMaps[issue.RepoID] = issue.Repo + } + if issue.PullRequest != nil && issue.PullRequest.BaseRepo == nil { issue.PullRequest.BaseRepo = issue.Repo } } diff --git a/models/issue_test.go b/models/issue_test.go index eadeb66ab9..cebf20af9b 100644 --- a/models/issue_test.go +++ b/models/issue_test.go @@ -206,11 +206,26 @@ func TestGetUserIssueStats(t *testing.T) { FilterMode: FilterModeAll, }, IssueStats{ - YourRepositoriesCount: 0, - AssignCount: 1, - CreateCount: 1, - OpenCount: 0, - ClosedCount: 0, + YourRepositoriesCount: 1, // 6 + AssignCount: 1, // 6 + CreateCount: 1, // 6 + OpenCount: 1, // 6 + ClosedCount: 1, // 1 + }, + }, + { + UserIssueStatsOptions{ + UserID: 1, + RepoIDs: []int64{1}, + FilterMode: FilterModeAll, + IsClosed: true, + }, + IssueStats{ + YourRepositoriesCount: 1, // 6 + AssignCount: 0, + CreateCount: 0, + OpenCount: 1, // 6 + ClosedCount: 1, // 1 }, }, { @@ -219,10 +234,10 @@ func TestGetUserIssueStats(t *testing.T) { FilterMode: FilterModeAssign, }, IssueStats{ - YourRepositoriesCount: 0, - AssignCount: 2, - CreateCount: 2, - OpenCount: 2, + YourRepositoriesCount: 1, // 6 + AssignCount: 1, // 6 + CreateCount: 1, // 6 + OpenCount: 1, // 6 ClosedCount: 0, }, }, @@ -232,37 +247,23 @@ func TestGetUserIssueStats(t *testing.T) { FilterMode: FilterModeCreate, }, IssueStats{ - YourRepositoriesCount: 0, - AssignCount: 2, - CreateCount: 2, - OpenCount: 2, + YourRepositoriesCount: 1, // 6 + AssignCount: 1, // 6 + CreateCount: 1, // 6 + OpenCount: 1, // 6 ClosedCount: 0, }, }, - { - UserIssueStatsOptions{ - UserID: 2, - UserRepoIDs: []int64{1, 2}, - FilterMode: FilterModeAll, - IsClosed: true, - }, - IssueStats{ - YourRepositoriesCount: 2, - AssignCount: 0, - CreateCount: 2, - OpenCount: 2, - ClosedCount: 2, - }, - }, { UserIssueStatsOptions{ UserID: 1, FilterMode: FilterModeMention, }, IssueStats{ - YourRepositoriesCount: 0, - AssignCount: 2, - CreateCount: 2, + YourRepositoriesCount: 1, // 6 + AssignCount: 1, // 6 + CreateCount: 1, // 6 + MentionCount: 0, OpenCount: 0, ClosedCount: 0, }, @@ -274,19 +275,21 @@ func TestGetUserIssueStats(t *testing.T) { IssueIDs: []int64{1}, }, IssueStats{ - YourRepositoriesCount: 0, - AssignCount: 1, - CreateCount: 1, - OpenCount: 1, + YourRepositoriesCount: 1, // 1 + AssignCount: 1, // 1 + CreateCount: 1, // 1 + OpenCount: 1, // 1 ClosedCount: 0, }, }, } { - stats, err := GetUserIssueStats(test.Opts) - if !assert.NoError(t, err) { - continue - } - assert.Equal(t, test.ExpectedIssueStats, *stats) + t.Run(fmt.Sprintf("%#v", test.Opts), func(t *testing.T) { + stats, err := GetUserIssueStats(test.Opts) + if !assert.NoError(t, err) { + return + } + assert.Equal(t, test.ExpectedIssueStats, *stats) + }) } } diff --git a/models/repo/repo.go b/models/repo/repo.go index d0136e9c66..5108231cd8 100644 --- a/models/repo/repo.go +++ b/models/repo/repo.go @@ -731,15 +731,6 @@ func CountUserRepositories(userID int64, private bool) int64 { return countRepositories(userID, private) } -// GetUserMirrorRepositories returns a list of mirror repositories of given user. -func GetUserMirrorRepositories(userID int64) ([]*Repository, error) { - repos := make([]*Repository, 0, 10) - return repos, db.GetEngine(db.DefaultContext). - Where("owner_id = ?", userID). - And("is_mirror = ?", true). - Find(&repos) -} - func getRepositoryCount(e db.Engine, ownerID int64) (int64, error) { return e.Count(&Repository{OwnerID: ownerID}) } @@ -766,25 +757,3 @@ func GetPublicRepositoryCount(u *user_model.User) (int64, error) { func GetPrivateRepositoryCount(u *user_model.User) (int64, error) { return getPrivateRepositoryCount(db.GetEngine(db.DefaultContext), u) } - -// IterateRepository iterate repositories -func IterateRepository(f func(repo *Repository) error) error { - var start int - batchSize := setting.Database.IterateBufferSize - for { - repos := make([]*Repository, 0, batchSize) - if err := db.GetEngine(db.DefaultContext).Limit(batchSize, start).Find(&repos); err != nil { - return err - } - if len(repos) == 0 { - return nil - } - start += len(repos) - - for _, repo := range repos { - if err := f(repo); err != nil { - return err - } - } - } -} diff --git a/models/repo/repo_list.go b/models/repo/repo_list.go new file mode 100644 index 0000000000..571604a2c2 --- /dev/null +++ b/models/repo/repo_list.go @@ -0,0 +1,46 @@ +// Copyright 2021 The Gitea Authors. All rights reserved. +// Use of this source code is governed by a MIT-style +// license that can be found in the LICENSE file. + +package repo + +import ( + "code.gitea.io/gitea/models/db" + "code.gitea.io/gitea/modules/setting" +) + +// GetUserMirrorRepositories returns a list of mirror repositories of given user. +func GetUserMirrorRepositories(userID int64) ([]*Repository, error) { + repos := make([]*Repository, 0, 10) + return repos, db.GetEngine(db.DefaultContext). + Where("owner_id = ?", userID). + And("is_mirror = ?", true). + Find(&repos) +} + +// IterateRepository iterate repositories +func IterateRepository(f func(repo *Repository) error) error { + var start int + batchSize := setting.Database.IterateBufferSize + for { + repos := make([]*Repository, 0, batchSize) + if err := db.GetEngine(db.DefaultContext).Limit(batchSize, start).Find(&repos); err != nil { + return err + } + if len(repos) == 0 { + return nil + } + start += len(repos) + + for _, repo := range repos { + if err := f(repo); err != nil { + return err + } + } + } +} + +// FindReposMapByIDs find repos as map +func FindReposMapByIDs(repoIDs []int64, res map[int64]*Repository) error { + return db.GetEngine(db.DefaultContext).In("id", repoIDs).Find(&res) +} diff --git a/models/repo_list.go b/models/repo_list.go index 02440bec93..9bda0d5a37 100644 --- a/models/repo_list.go +++ b/models/repo_list.go @@ -11,6 +11,7 @@ import ( "code.gitea.io/gitea/models/db" "code.gitea.io/gitea/models/perm" repo_model "code.gitea.io/gitea/models/repo" + "code.gitea.io/gitea/models/unit" user_model "code.gitea.io/gitea/models/user" "code.gitea.io/gitea/modules/structs" "code.gitea.io/gitea/modules/util" @@ -145,6 +146,188 @@ type SearchRepoOptions struct { LowerNames []string } +// SearchOrderBy is used to sort the result +type SearchOrderBy string + +func (s SearchOrderBy) String() string { + return string(s) +} + +// Strings for sorting result +const ( + SearchOrderByAlphabetically SearchOrderBy = "name ASC" + SearchOrderByAlphabeticallyReverse SearchOrderBy = "name DESC" + SearchOrderByLeastUpdated SearchOrderBy = "updated_unix ASC" + SearchOrderByRecentUpdated SearchOrderBy = "updated_unix DESC" + SearchOrderByOldest SearchOrderBy = "created_unix ASC" + SearchOrderByNewest SearchOrderBy = "created_unix DESC" + SearchOrderBySize SearchOrderBy = "size ASC" + SearchOrderBySizeReverse SearchOrderBy = "size DESC" + SearchOrderByID SearchOrderBy = "id ASC" + SearchOrderByIDReverse SearchOrderBy = "id DESC" + SearchOrderByStars SearchOrderBy = "num_stars ASC" + SearchOrderByStarsReverse SearchOrderBy = "num_stars DESC" + SearchOrderByForks SearchOrderBy = "num_forks ASC" + SearchOrderByForksReverse SearchOrderBy = "num_forks DESC" +) + +// userOwnedRepoCond returns user ownered repositories +func userOwnedRepoCond(userID int64) builder.Cond { + return builder.Eq{ + "repository.owner_id": userID, + } +} + +// userAssignedRepoCond return user as assignee repositories list +func userAssignedRepoCond(id string, userID int64) builder.Cond { + return builder.And( + builder.Eq{ + "repository.is_private": false, + }, + builder.In(id, + builder.Select("issue.repo_id").From("issue_assignees"). + InnerJoin("issue", "issue.id = issue_assignees.issue_id"). + Where(builder.Eq{ + "issue_assignees.assignee_id": userID, + }), + ), + ) +} + +// userCreateIssueRepoCond return user created issues repositories list +func userCreateIssueRepoCond(id string, userID int64, isPull bool) builder.Cond { + return builder.And( + builder.Eq{ + "repository.is_private": false, + }, + builder.In(id, + builder.Select("issue.repo_id").From("issue"). + Where(builder.Eq{ + "issue.poster_id": userID, + "issue.is_pull": isPull, + }), + ), + ) +} + +// userMentionedRepoCond return user metinoed repositories list +func userMentionedRepoCond(id string, userID int64) builder.Cond { + return builder.And( + builder.Eq{ + "repository.is_private": false, + }, + builder.In(id, + builder.Select("issue.repo_id").From("issue_user"). + InnerJoin("issue", "issue.id = issue_user.issue_id"). + Where(builder.Eq{ + "issue_user.is_mentioned": true, + "issue_user.uid": userID, + }), + ), + ) +} + +// teamUnitsRepoCond returns query condition for those repo id in the special org team with special units access +func teamUnitsRepoCond(id string, userID, orgID, teamID int64, units ...unit.Type) builder.Cond { + return builder.In(id, + builder.Select("repo_id").From("team_repo").Where( + builder.Eq{ + "team_id": teamID, + }.And( + builder.In( + "team_id", builder.Select("team_id").From("team_user").Where( + builder.Eq{ + "uid": userID, + }, + ), + )).And( + builder.In( + "team_id", builder.Select("team_id").From("team_unit").Where( + builder.Eq{ + "org_id": orgID, + }.And( + builder.In("type", units), + ), + ), + ), + ), + )) +} + +// userCollaborationRepoCond returns user as collabrators repositories list +func userCollaborationRepoCond(idStr string, userID int64) builder.Cond { + return builder.In(idStr, builder.Select("repo_id"). + From("`access`"). + Where(builder.And( + builder.Eq{"user_id": userID}, + builder.Gt{"mode": int(perm.AccessModeNone)}, + )), + ) +} + +// userOrgTeamRepoCond selects repos that the given user has access to through team membership +func userOrgTeamRepoCond(idStr string, userID int64) builder.Cond { + return builder.In(idStr, userOrgTeamRepoBuilder(userID)) +} + +// userOrgTeamRepoBuilder returns repo ids where user's teams can access. +func userOrgTeamRepoBuilder(userID int64) *builder.Builder { + return builder.Select("`team_repo`.repo_id"). + From("team_repo"). + Join("INNER", "team_user", "`team_user`.team_id = `team_repo`.team_id"). + Where(builder.Eq{"`team_user`.uid": userID}) +} + +// userOrgTeamUnitRepoBuilder returns repo ids where user's teams can access the special unit. +func userOrgTeamUnitRepoBuilder(userID int64, unitType unit.Type) *builder.Builder { + return userOrgTeamRepoBuilder(userID). + Join("INNER", "team_unit", "`team_unit`.team_id = `team_repo`.team_id"). + Where(builder.Eq{"`team_unit`.`type`": unitType}) +} + +// userOrgUnitRepoCond selects repos that the given user has access to through org and the special unit +func userOrgUnitRepoCond(idStr string, userID, orgID int64, unitType unit.Type) builder.Cond { + return builder.In(idStr, + userOrgTeamUnitRepoBuilder(userID, unitType). + And(builder.Eq{"org_id": orgID}), + ) +} + +// userOrgPublicRepoCond returns the condition that one user could access all public repositories in organizations +func userOrgPublicRepoCond(userID int64) builder.Cond { + return builder.And( + builder.Eq{"`repository`.is_private": false}, + builder.In("`repository`.owner_id", + builder.Select("`org_user`.org_id"). + From("org_user"). + Where(builder.Eq{"`org_user`.uid": userID}), + ), + ) +} + +// userOrgPublicRepoCondPrivate returns the condition that one user could access all public repositories in private organizations +func userOrgPublicRepoCondPrivate(userID int64) builder.Cond { + return builder.And( + builder.Eq{"`repository`.is_private": false}, + builder.In("`repository`.owner_id", + builder.Select("`org_user`.org_id"). + From("org_user"). + Join("INNER", "`user`", "`user`.id = `org_user`.org_id"). + Where(builder.Eq{ + "`org_user`.uid": userID, + "`user`.`type`": user_model.UserTypeOrganization, + "`user`.visibility": structs.VisibleTypePrivate, + }), + ), + ) +} + +// userOrgPublicUnitRepoCond returns the condition that one user could access all public repositories in the special organization +func userOrgPublicUnitRepoCond(userID, orgID int64) builder.Cond { + return userOrgPublicRepoCond(userID). + And(builder.Eq{"`repository`.owner_id": orgID}) +} + // SearchRepositoryCondition creates a query condition according search repository options func SearchRepositoryCondition(opts *SearchRepoOptions) builder.Cond { cond := builder.NewCond() @@ -198,27 +381,12 @@ func SearchRepositoryCondition(opts *SearchRepoOptions) builder.Cond { // 2. But we can see because of: builder.Or( // A. We have access - builder.In("`repository`.id", - builder.Select("`access`.repo_id"). - From("access"). - Where(builder.Eq{"`access`.user_id": opts.OwnerID})), + userCollaborationRepoCond("`repository`.id", opts.OwnerID), // B. We are in a team for - builder.In("`repository`.id", builder.Select("`team_repo`.repo_id"). - From("team_repo"). - Where(builder.Eq{"`team_user`.uid": opts.OwnerID}). - Join("INNER", "team_user", "`team_user`.team_id = `team_repo`.team_id")), - // C. Public repositories in private organizations that we are member of - builder.And( - builder.Eq{"`repository`.is_private": false}, - builder.In("`repository`.owner_id", - builder.Select("`org_user`.org_id"). - From("org_user"). - Join("INNER", "`user`", "`user`.id = `org_user`.org_id"). - Where(builder.Eq{ - "`org_user`.uid": opts.OwnerID, - "`user`.type": user_model.UserTypeOrganization, - "`user`.visibility": structs.VisibleTypePrivate, - })))), + userOrgTeamRepoCond("`repository`.id", opts.OwnerID), + // C. Public repositories in organizations that we are member of + userOrgPublicRepoCondPrivate(opts.OwnerID), + ), ) if !opts.Private { 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)) @@ -389,24 +557,14 @@ func accessibleRepositoryCondition(user *user_model.User) builder.Cond { if user != nil { cond = cond.Or( // 2. Be able to see all repositories that we have access to - builder.In("`repository`.id", builder.Select("repo_id"). - From("`access`"). - Where(builder.And( - builder.Eq{"user_id": user.ID}, - builder.Gt{"mode": int(perm.AccessModeNone)}))), + userCollaborationRepoCond("`repository`.id", user.ID), // 3. Repositories that we directly own builder.Eq{"`repository`.owner_id": user.ID}, // 4. Be able to see all repositories that we are in a team - builder.In("`repository`.id", builder.Select("`team_repo`.repo_id"). - From("team_repo"). - Where(builder.Eq{"`team_user`.uid": user.ID}). - Join("INNER", "team_user", "`team_user`.team_id = `team_repo`.team_id")), + userOrgTeamRepoCond("`repository`.id", user.ID), // 5. Be able to see all public repos in private organizations that we are an org_user of - builder.And(builder.Eq{"`repository`.is_private": false}, - builder.In("`repository`.owner_id", - builder.Select("`org_user`.org_id"). - From("org_user"). - Where(builder.Eq{"`org_user`.uid": user.ID})))) + userOrgPublicRepoCond(user.ID), + ) } return cond diff --git a/models/repo_permission.go b/models/repo_permission.go index 45878c8ba4..40b63aa804 100644 --- a/models/repo_permission.go +++ b/models/repo_permission.go @@ -400,26 +400,6 @@ func HasAccess(userID int64, repo *repo_model.Repository) (bool, error) { return hasAccess(db.DefaultContext, userID, repo) } -// FilterOutRepoIdsWithoutUnitAccess filter out repos where user has no access to repositories -func FilterOutRepoIdsWithoutUnitAccess(u *user_model.User, repoIDs []int64, units ...unit.Type) ([]int64, error) { - i := 0 - for _, rID := range repoIDs { - repo, err := repo_model.GetRepositoryByID(rID) - if err != nil { - return nil, err - } - perm, err := GetUserRepoPermission(repo, u) - if err != nil { - return nil, err - } - if perm.CanReadAny(units...) { - repoIDs[i] = rID - i++ - } - } - return repoIDs[:i], nil -} - // GetRepoReaders returns all users that have explicit read access or higher to the repository. func GetRepoReaders(repo *repo_model.Repository) (_ []*user_model.User, err error) { return getUsersWithAccessMode(db.DefaultContext, repo, perm_model.AccessModeRead) diff --git a/models/user.go b/models/user.go index 2e7a84273f..5f7bedd36d 100644 --- a/models/user.go +++ b/models/user.go @@ -15,7 +15,6 @@ import ( asymkey_model "code.gitea.io/gitea/models/asymkey" "code.gitea.io/gitea/models/db" repo_model "code.gitea.io/gitea/models/repo" - "code.gitea.io/gitea/models/unit" user_model "code.gitea.io/gitea/models/user" "code.gitea.io/gitea/modules/setting" "code.gitea.io/gitea/modules/structs" @@ -30,109 +29,6 @@ func GetOrganizationCount(ctx context.Context, u *user_model.User) (int64, error Count(new(OrgUser)) } -// GetRepositoryIDs returns repositories IDs where user owned and has unittypes -// Caller shall check that units is not globally disabled -func GetRepositoryIDs(u *user_model.User, units ...unit.Type) ([]int64, error) { - var ids []int64 - - sess := db.GetEngine(db.DefaultContext).Table("repository").Cols("repository.id") - - if len(units) > 0 { - sess = sess.Join("INNER", "repo_unit", "repository.id = repo_unit.repo_id") - sess = sess.In("repo_unit.type", units) - } - - return ids, sess.Where("owner_id = ?", u.ID).Find(&ids) -} - -// GetActiveRepositoryIDs returns non-archived repositories IDs where user owned and has unittypes -// Caller shall check that units is not globally disabled -func GetActiveRepositoryIDs(u *user_model.User, units ...unit.Type) ([]int64, error) { - var ids []int64 - - sess := db.GetEngine(db.DefaultContext).Table("repository").Cols("repository.id") - - if len(units) > 0 { - sess = sess.Join("INNER", "repo_unit", "repository.id = repo_unit.repo_id") - sess = sess.In("repo_unit.type", units) - } - - sess.Where(builder.Eq{"is_archived": false}) - - return ids, sess.Where("owner_id = ?", u.ID).GroupBy("repository.id").Find(&ids) -} - -// GetOrgRepositoryIDs returns repositories IDs where user's team owned and has unittypes -// Caller shall check that units is not globally disabled -func GetOrgRepositoryIDs(u *user_model.User, units ...unit.Type) ([]int64, error) { - var ids []int64 - - if err := db.GetEngine(db.DefaultContext).Table("repository"). - Cols("repository.id"). - Join("INNER", "team_user", "repository.owner_id = team_user.org_id"). - Join("INNER", "team_repo", "(? != ? and repository.is_private != ?) OR (team_user.team_id = team_repo.team_id AND repository.id = team_repo.repo_id)", true, u.IsRestricted, true). - Where("team_user.uid = ?", u.ID). - GroupBy("repository.id").Find(&ids); err != nil { - return nil, err - } - - if len(units) > 0 { - return FilterOutRepoIdsWithoutUnitAccess(u, ids, units...) - } - - return ids, nil -} - -// GetActiveOrgRepositoryIDs returns non-archived repositories IDs where user's team owned and has unittypes -// Caller shall check that units is not globally disabled -func GetActiveOrgRepositoryIDs(u *user_model.User, units ...unit.Type) ([]int64, error) { - var ids []int64 - - if err := db.GetEngine(db.DefaultContext).Table("repository"). - Cols("repository.id"). - Join("INNER", "team_user", "repository.owner_id = team_user.org_id"). - Join("INNER", "team_repo", "(? != ? and repository.is_private != ?) OR (team_user.team_id = team_repo.team_id AND repository.id = team_repo.repo_id)", true, u.IsRestricted, true). - Where("team_user.uid = ?", u.ID). - Where(builder.Eq{"is_archived": false}). - GroupBy("repository.id").Find(&ids); err != nil { - return nil, err - } - - if len(units) > 0 { - return FilterOutRepoIdsWithoutUnitAccess(u, ids, units...) - } - - return ids, nil -} - -// GetAccessRepoIDs returns all repositories IDs where user's or user is a team member organizations -// Caller shall check that units is not globally disabled -func GetAccessRepoIDs(u *user_model.User, units ...unit.Type) ([]int64, error) { - ids, err := GetRepositoryIDs(u, units...) - if err != nil { - return nil, err - } - ids2, err := GetOrgRepositoryIDs(u, units...) - if err != nil { - return nil, err - } - return append(ids, ids2...), nil -} - -// GetActiveAccessRepoIDs returns all non-archived repositories IDs where user's or user is a team member organizations -// Caller shall check that units is not globally disabled -func GetActiveAccessRepoIDs(u *user_model.User, units ...unit.Type) ([]int64, error) { - ids, err := GetActiveRepositoryIDs(u, units...) - if err != nil { - return nil, err - } - ids2, err := GetActiveOrgRepositoryIDs(u, units...) - if err != nil { - return nil, err - } - return append(ids, ids2...), nil -} - // deleteBeans deletes all given beans, beans should contain delete conditions. func deleteBeans(e db.Engine, beans ...interface{}) (err error) { for i := range beans { diff --git a/models/user_test.go b/models/user_test.go index 4749a3af73..83201ff4cb 100644 --- a/models/user_test.go +++ b/models/user_test.go @@ -98,25 +98,3 @@ func testIsUserOrgOwner(t *testing.T, uid, orgID int64, expected bool) { assert.NoError(t, err) assert.Equal(t, expected, is) } - -func TestGetOrgRepositoryIDs(t *testing.T) { - assert.NoError(t, unittest.PrepareTestDatabase()) - user2 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2}).(*user_model.User) - user4 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 4}).(*user_model.User) - user5 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 5}).(*user_model.User) - - accessibleRepos, err := GetOrgRepositoryIDs(user2) - assert.NoError(t, err) - // User 2's team has access to private repos 3, 5, repo 32 is a public repo of the organization - assert.Equal(t, []int64{3, 5, 23, 24, 32}, accessibleRepos) - - accessibleRepos, err = GetOrgRepositoryIDs(user4) - assert.NoError(t, err) - // User 4's team has access to private repo 3, repo 32 is a public repo of the organization - assert.Equal(t, []int64{3, 32}, accessibleRepos) - - accessibleRepos, err = GetOrgRepositoryIDs(user5) - assert.NoError(t, err) - // User 5's team has no access to any repo - assert.Len(t, accessibleRepos, 0) -} diff --git a/routers/web/user/home.go b/routers/web/user/home.go index 975262cf9c..8bbc6dd0c3 100644 --- a/routers/web/user/home.go +++ b/routers/web/user/home.go @@ -403,27 +403,26 @@ func buildIssueOverview(ctx *context.Context, unitType unit.Type) { // - Count Issues by repo // -------------------------------------------------------------------------- + // Get repository IDs where User/Org/Team has access. + var team *models.Team + var org *models.Organization + if ctx.Org != nil { + org = ctx.Org.Organization + team = ctx.Org.Team + } + isPullList := unitType == unit.TypePullRequests opts := &models.IssuesOptions{ IsPull: util.OptionalBoolOf(isPullList), SortType: sortType, IsArchived: util.OptionalBoolFalse, - } - - // Get repository IDs where User/Org/Team has access. - var team *models.Team - if ctx.Org != nil { - team = ctx.Org.Team - } - userRepoIDs, err := getActiveUserRepoIDs(ctxUser, team, unitType) - if err != nil { - ctx.ServerError("userRepoIDs", err) - return + Org: org, + Team: team, + User: ctx.User, } switch filterMode { case models.FilterModeAll: - opts.RepoIDs = userRepoIDs case models.FilterModeAssign: opts.AssigneeID = ctx.User.ID case models.FilterModeCreate: @@ -434,10 +433,6 @@ func buildIssueOverview(ctx *context.Context, unitType unit.Type) { opts.ReviewRequestedID = ctx.User.ID } - if ctxUser.IsOrganization() { - opts.RepoIDs = userRepoIDs - } - // keyword holds the search term entered into the search field. keyword := strings.Trim(ctx.FormString("q"), " ") ctx.Data["Keyword"] = keyword @@ -524,27 +519,25 @@ func buildIssueOverview(ctx *context.Context, unitType unit.Type) { // ---------------------------------- // showReposMap maps repository IDs to their Repository pointers. - showReposMap, err := repoIDMap(ctxUser, issueCountByRepo, unitType) + showReposMap, err := loadRepoByIDs(ctxUser, issueCountByRepo, unitType) if err != nil { if repo_model.IsErrRepoNotExist(err) { ctx.NotFound("GetRepositoryByID", err) return } - ctx.ServerError("repoIDMap", err) + ctx.ServerError("loadRepoByIDs", err) return } // a RepositoryList showRepos := models.RepositoryListOfMap(showReposMap) sort.Sort(showRepos) - if err = showRepos.LoadAttributes(); err != nil { - ctx.ServerError("LoadAttributes", err) - return - } // maps pull request IDs to their CommitStatus. Will be posted to ctx.Data. for _, issue := range issues { - issue.Repo = showReposMap[issue.RepoID] + if issue.Repo == nil { + issue.Repo = showReposMap[issue.RepoID] + } } commitStatus, err := pull_service.GetIssuesLastCommitStatus(issues) @@ -556,86 +549,39 @@ func buildIssueOverview(ctx *context.Context, unitType unit.Type) { // ------------------------------- // Fill stats to post to ctx.Data. // ------------------------------- - - userIssueStatsOpts := models.UserIssueStatsOptions{ - UserID: ctx.User.ID, - UserRepoIDs: userRepoIDs, - FilterMode: filterMode, - IsPull: isPullList, - IsClosed: isShowClosed, - IsArchived: util.OptionalBoolFalse, - LabelIDs: opts.LabelIDs, - } - if len(repoIDs) > 0 { - userIssueStatsOpts.UserRepoIDs = repoIDs - } - if ctxUser.IsOrganization() { - userIssueStatsOpts.RepoIDs = userRepoIDs - } - userIssueStats, err := models.GetUserIssueStats(userIssueStatsOpts) - if err != nil { - ctx.ServerError("GetUserIssueStats User", err) - return - } - - var shownIssueStats *models.IssueStats + var issueStats *models.IssueStats if !forceEmpty { statsOpts := models.UserIssueStatsOptions{ - UserID: ctx.User.ID, - UserRepoIDs: userRepoIDs, - FilterMode: filterMode, - IsPull: isPullList, - IsClosed: isShowClosed, - IssueIDs: issueIDsFromSearch, - IsArchived: util.OptionalBoolFalse, - LabelIDs: opts.LabelIDs, + UserID: ctx.User.ID, + FilterMode: filterMode, + IsPull: isPullList, + IsClosed: isShowClosed, + IssueIDs: issueIDsFromSearch, + IsArchived: util.OptionalBoolFalse, + LabelIDs: opts.LabelIDs, + Org: org, + Team: team, } if len(repoIDs) > 0 { statsOpts.RepoIDs = repoIDs - } else if ctxUser.IsOrganization() { - statsOpts.RepoIDs = userRepoIDs } - shownIssueStats, err = models.GetUserIssueStats(statsOpts) + issueStats, err = models.GetUserIssueStats(statsOpts) if err != nil { ctx.ServerError("GetUserIssueStats Shown", err) return } } else { - shownIssueStats = &models.IssueStats{} - } - - var allIssueStats *models.IssueStats - if !forceEmpty { - allIssueStatsOpts := models.UserIssueStatsOptions{ - UserID: ctx.User.ID, - UserRepoIDs: userRepoIDs, - FilterMode: filterMode, - IsPull: isPullList, - IsClosed: isShowClosed, - IssueIDs: issueIDsFromSearch, - IsArchived: util.OptionalBoolFalse, - LabelIDs: opts.LabelIDs, - } - if ctxUser.IsOrganization() { - allIssueStatsOpts.RepoIDs = userRepoIDs - } - allIssueStats, err = models.GetUserIssueStats(allIssueStatsOpts) - if err != nil { - ctx.ServerError("GetUserIssueStats All", err) - return - } - } else { - allIssueStats = &models.IssueStats{} + issueStats = &models.IssueStats{} } // Will be posted to ctx.Data. var shownIssues int if !isShowClosed { - shownIssues = int(shownIssueStats.OpenCount) - ctx.Data["TotalIssueCount"] = int(allIssueStats.OpenCount) + shownIssues = int(issueStats.OpenCount) + ctx.Data["TotalIssueCount"] = shownIssues } else { - shownIssues = int(shownIssueStats.ClosedCount) - ctx.Data["TotalIssueCount"] = int(allIssueStats.ClosedCount) + shownIssues = int(issueStats.ClosedCount) + ctx.Data["TotalIssueCount"] = shownIssues } ctx.Data["IsShowClosed"] = isShowClosed @@ -671,8 +617,7 @@ func buildIssueOverview(ctx *context.Context, unitType unit.Type) { ctx.Data["CommitStatus"] = commitStatus ctx.Data["Repos"] = showRepos ctx.Data["Counts"] = issueCountByRepo - ctx.Data["IssueStats"] = userIssueStats - ctx.Data["ShownIssueStats"] = shownIssueStats + ctx.Data["IssueStats"] = issueStats ctx.Data["ViewType"] = viewType ctx.Data["SortType"] = sortType ctx.Data["RepoIDs"] = repoIDs @@ -730,56 +675,6 @@ func getRepoIDs(reposQuery string) []int64 { return repoIDs } -func getActiveUserRepoIDs(ctxUser *user_model.User, team *models.Team, unitType unit.Type) ([]int64, error) { - var userRepoIDs []int64 - var err error - - if ctxUser.IsOrganization() { - userRepoIDs, err = getActiveTeamOrOrgRepoIds(ctxUser, team, unitType) - if err != nil { - return nil, fmt.Errorf("orgRepoIds: %v", err) - } - } else { - userRepoIDs, err = models.GetActiveAccessRepoIDs(ctxUser, unitType) - if err != nil { - return nil, fmt.Errorf("ctxUser.GetAccessRepoIDs: %v", err) - } - } - - if len(userRepoIDs) == 0 { - userRepoIDs = []int64{-1} - } - - return userRepoIDs, nil -} - -// getActiveTeamOrOrgRepoIds gets RepoIDs for ctxUser as Organization. -// Should be called if and only if ctxUser.IsOrganization == true. -func getActiveTeamOrOrgRepoIds(ctxUser *user_model.User, team *models.Team, unitType unit.Type) ([]int64, error) { - var orgRepoIDs []int64 - var err error - var env models.AccessibleReposEnvironment - - if team != nil { - env = models.OrgFromUser(ctxUser).AccessibleTeamReposEnv(team) - } else { - env, err = models.OrgFromUser(ctxUser).AccessibleReposEnv(ctxUser.ID) - if err != nil { - return nil, fmt.Errorf("AccessibleReposEnv: %v", err) - } - } - orgRepoIDs, err = env.RepoIDs(1, ctxUser.NumRepos) - if err != nil { - return nil, fmt.Errorf("env.RepoIDs: %v", err) - } - orgRepoIDs, err = models.FilterOutRepoIdsWithoutUnitAccess(ctxUser, orgRepoIDs, unitType) - if err != nil { - return nil, fmt.Errorf("FilterOutRepoIdsWithoutUnitAccess: %v", err) - } - - return orgRepoIDs, nil -} - func issueIDsFromSearch(ctxUser *user_model.User, keyword string, opts *models.IssuesOptions) ([]int64, error) { if len(keyword) == 0 { return []int64{}, nil @@ -797,33 +692,27 @@ func issueIDsFromSearch(ctxUser *user_model.User, keyword string, opts *models.I return issueIDsFromSearch, nil } -func repoIDMap(ctxUser *user_model.User, issueCountByRepo map[int64]int64, unitType unit.Type) (map[int64]*repo_model.Repository, error) { - repoByID := make(map[int64]*repo_model.Repository, len(issueCountByRepo)) +func loadRepoByIDs(ctxUser *user_model.User, issueCountByRepo map[int64]int64, unitType unit.Type) (map[int64]*repo_model.Repository, error) { + var totalRes = make(map[int64]*repo_model.Repository, len(issueCountByRepo)) + var repoIDs = make([]int64, 0, 500) for id := range issueCountByRepo { if id <= 0 { continue } - if _, ok := repoByID[id]; !ok { - repo, err := repo_model.GetRepositoryByID(id) - if repo_model.IsErrRepoNotExist(err) { + repoIDs = append(repoIDs, id) + if len(repoIDs) == 500 { + if err := repo_model.FindReposMapByIDs(repoIDs, totalRes); err != nil { return nil, err - } else if err != nil { - return nil, fmt.Errorf("GetRepositoryByID: [%d]%v", id, err) } - repoByID[id] = repo - } - repo := repoByID[id] - - // Check if user has access to given repository. - perm, err := models.GetUserRepoPermission(repo, ctxUser) - if err != nil { - return nil, fmt.Errorf("GetUserRepoPermission: [%d]%v", id, err) - } - if !perm.CanRead(unitType) { - log.Debug("User created Issues in Repository which they no longer have access to: [%d]", id) + repoIDs = repoIDs[:0] } } - return repoByID, nil + if len(repoIDs) > 0 { + if err := repo_model.FindReposMapByIDs(repoIDs, totalRes); err != nil { + return nil, err + } + } + return totalRes, nil } // ShowSSHKeys output all the ssh keys of user by uid diff --git a/templates/user/dashboard/issues.tmpl b/templates/user/dashboard/issues.tmpl index aca61f9ae9..dc66859b83 100644 --- a/templates/user/dashboard/issues.tmpl +++ b/templates/user/dashboard/issues.tmpl @@ -61,11 +61,11 @@