This reverts commit 78438d310b.
tags/v1.11.0-dev
| @@ -1306,19 +1306,18 @@ func GetIssuesByIDs(issueIDs []int64) ([]*Issue, error) { | |||||
| // IssuesOptions represents options of an issue. | // IssuesOptions represents options of an issue. | ||||
| type IssuesOptions struct { | type IssuesOptions struct { | ||||
| RepoIDs []int64 // include all repos if empty | |||||
| RepoSubQuery *builder.Builder | |||||
| AssigneeID int64 | |||||
| PosterID int64 | |||||
| MentionedID int64 | |||||
| MilestoneID int64 | |||||
| Page int | |||||
| PageSize int | |||||
| IsClosed util.OptionalBool | |||||
| IsPull util.OptionalBool | |||||
| LabelIDs []int64 | |||||
| SortType string | |||||
| IssueIDs []int64 | |||||
| RepoIDs []int64 // include all repos if empty | |||||
| AssigneeID int64 | |||||
| PosterID int64 | |||||
| MentionedID int64 | |||||
| MilestoneID int64 | |||||
| Page int | |||||
| PageSize int | |||||
| IsClosed util.OptionalBool | |||||
| IsPull util.OptionalBool | |||||
| LabelIDs []int64 | |||||
| SortType string | |||||
| IssueIDs []int64 | |||||
| } | } | ||||
| // sortIssuesSession sort an issues-related session based on the provided | // sortIssuesSession sort an issues-related session based on the provided | ||||
| @@ -1361,9 +1360,7 @@ func (opts *IssuesOptions) setupSession(sess *xorm.Session) { | |||||
| sess.In("issue.id", opts.IssueIDs) | sess.In("issue.id", opts.IssueIDs) | ||||
| } | } | ||||
| if opts.RepoSubQuery != nil { | |||||
| sess.In("issue.repo_id", opts.RepoSubQuery) | |||||
| } else if len(opts.RepoIDs) > 0 { | |||||
| if len(opts.RepoIDs) > 0 { | |||||
| // In case repository IDs are provided but actually no repository has issue. | // In case repository IDs are provided but actually no repository has issue. | ||||
| sess.In("issue.repo_id", opts.RepoIDs) | sess.In("issue.repo_id", opts.RepoIDs) | ||||
| } | } | ||||
| @@ -1630,12 +1627,12 @@ func GetIssueStats(opts *IssueStatsOptions) (*IssueStats, error) { | |||||
| // UserIssueStatsOptions contains parameters accepted by GetUserIssueStats. | // UserIssueStatsOptions contains parameters accepted by GetUserIssueStats. | ||||
| type UserIssueStatsOptions struct { | type UserIssueStatsOptions struct { | ||||
| UserID int64 | |||||
| RepoID int64 | |||||
| RepoSubQuery *builder.Builder | |||||
| FilterMode int | |||||
| IsPull bool | |||||
| IsClosed bool | |||||
| UserID int64 | |||||
| RepoID int64 | |||||
| UserRepoIDs []int64 | |||||
| FilterMode int | |||||
| IsPull bool | |||||
| IsClosed bool | |||||
| } | } | ||||
| // GetUserIssueStats returns issue statistic information for dashboard by given conditions. | // GetUserIssueStats returns issue statistic information for dashboard by given conditions. | ||||
| @@ -1649,23 +1646,16 @@ func GetUserIssueStats(opts UserIssueStatsOptions) (*IssueStats, error) { | |||||
| cond = cond.And(builder.Eq{"issue.repo_id": opts.RepoID}) | cond = cond.And(builder.Eq{"issue.repo_id": opts.RepoID}) | ||||
| } | } | ||||
| var repoCond = builder.NewCond() | |||||
| if opts.RepoSubQuery != nil { | |||||
| repoCond = builder.In("issue.repo_id", opts.RepoSubQuery) | |||||
| } else { | |||||
| repoCond = builder.Expr("0=1") | |||||
| } | |||||
| switch opts.FilterMode { | switch opts.FilterMode { | ||||
| case FilterModeAll: | case FilterModeAll: | ||||
| stats.OpenCount, err = x.Where(cond).And("is_closed = ?", false). | stats.OpenCount, err = x.Where(cond).And("is_closed = ?", false). | ||||
| And(repoCond). | |||||
| And(builder.In("issue.repo_id", opts.UserRepoIDs)). | |||||
| Count(new(Issue)) | Count(new(Issue)) | ||||
| if err != nil { | if err != nil { | ||||
| return nil, err | return nil, err | ||||
| } | } | ||||
| stats.ClosedCount, err = x.Where(cond).And("is_closed = ?", true). | stats.ClosedCount, err = x.Where(cond).And("is_closed = ?", true). | ||||
| And(repoCond). | |||||
| And(builder.In("issue.repo_id", opts.UserRepoIDs)). | |||||
| Count(new(Issue)) | Count(new(Issue)) | ||||
| if err != nil { | if err != nil { | ||||
| return nil, err | return nil, err | ||||
| @@ -1740,7 +1730,7 @@ func GetUserIssueStats(opts UserIssueStatsOptions) (*IssueStats, error) { | |||||
| } | } | ||||
| stats.YourRepositoriesCount, err = x.Where(cond). | stats.YourRepositoriesCount, err = x.Where(cond). | ||||
| And(repoCond). | |||||
| And(builder.In("issue.repo_id", opts.UserRepoIDs)). | |||||
| Count(new(Issue)) | Count(new(Issue)) | ||||
| if err != nil { | if err != nil { | ||||
| return nil, err | return nil, err | ||||
| @@ -10,7 +10,6 @@ import ( | |||||
| "time" | "time" | ||||
| "github.com/stretchr/testify/assert" | "github.com/stretchr/testify/assert" | ||||
| "xorm.io/builder" | |||||
| ) | ) | ||||
| func TestIssue_ReplaceLabels(t *testing.T) { | func TestIssue_ReplaceLabels(t *testing.T) { | ||||
| @@ -267,12 +266,10 @@ func TestGetUserIssueStats(t *testing.T) { | |||||
| }, | }, | ||||
| { | { | ||||
| UserIssueStatsOptions{ | UserIssueStatsOptions{ | ||||
| UserID: 2, | |||||
| RepoSubQuery: builder.Select("repository.id"). | |||||
| From("repository"). | |||||
| Where(builder.In("repository.id", []int64{1, 2})), | |||||
| FilterMode: FilterModeAll, | |||||
| IsClosed: true, | |||||
| UserID: 2, | |||||
| UserRepoIDs: []int64{1, 2}, | |||||
| FilterMode: FilterModeAll, | |||||
| IsClosed: true, | |||||
| }, | }, | ||||
| IssueStats{ | IssueStats{ | ||||
| YourRepositoriesCount: 2, | YourRepositoriesCount: 2, | ||||
| @@ -615,35 +615,50 @@ func (u *User) GetRepositories(page, pageSize int) (err error) { | |||||
| return err | return err | ||||
| } | } | ||||
| // UnitRepositoriesSubQuery returns repositories query builder according units | |||||
| func (u *User) UnitRepositoriesSubQuery(units ...UnitType) *builder.Builder { | |||||
| b := builder.Select("repository.id").From("repository") | |||||
| // GetRepositoryIDs returns repositories IDs where user owned and has unittypes | |||||
| func (u *User) GetRepositoryIDs(units ...UnitType) ([]int64, error) { | |||||
| var ids []int64 | |||||
| sess := x.Table("repository").Cols("repository.id") | |||||
| if len(units) > 0 { | if len(units) > 0 { | ||||
| b.Join("INNER", "repo_unit", builder.Expr("repository.id = repo_unit.repo_id"). | |||||
| And(builder.In("repo_unit.type", units)), | |||||
| ) | |||||
| sess = sess.Join("INNER", "repo_unit", "repository.id = repo_unit.repo_id") | |||||
| sess = sess.In("repo_unit.type", units) | |||||
| } | } | ||||
| return b.Where(builder.Eq{"repository.owner_id": u.ID}) | |||||
| return ids, sess.Where("owner_id = ?", u.ID).Find(&ids) | |||||
| } | } | ||||
| // OrgUnitRepositoriesSubQuery returns repositories query builder according orgnizations and units | |||||
| func (u *User) OrgUnitRepositoriesSubQuery(userID int64, units ...UnitType) *builder.Builder { | |||||
| b := builder. | |||||
| Select("team_repo.repo_id"). | |||||
| From("team_repo"). | |||||
| Join("INNER", "team_user", builder.Eq{"team_user.uid": userID}.And( | |||||
| builder.Expr("team_user.team_id = team_repo.team_id"), | |||||
| )) | |||||
| // GetOrgRepositoryIDs returns repositories IDs where user's team owned and has unittypes | |||||
| func (u *User) GetOrgRepositoryIDs(units ...UnitType) ([]int64, error) { | |||||
| var ids []int64 | |||||
| sess := x.Table("repository"). | |||||
| Cols("repository.id"). | |||||
| Join("INNER", "team_user", "repository.owner_id = team_user.org_id"). | |||||
| Join("INNER", "team_repo", "repository.is_private != ? OR (team_user.team_id = team_repo.team_id AND repository.id = team_repo.repo_id)", true) | |||||
| if len(units) > 0 { | if len(units) > 0 { | ||||
| b.Join("INNER", "team_unit", builder.Eq{"team_unit.org_id": u.ID}.And( | |||||
| builder.Expr("team_unit.team_id = team_repo.team_id").And( | |||||
| builder.In("`type`", units), | |||||
| ), | |||||
| )) | |||||
| } | |||||
| return b.Where(builder.Eq{"team_repo.org_id": u.ID}). | |||||
| GroupBy("team_repo.repo_id") | |||||
| sess = sess.Join("INNER", "team_unit", "team_unit.team_id = team_user.team_id") | |||||
| sess = sess.In("team_unit.type", units) | |||||
| } | |||||
| return ids, sess. | |||||
| Where("team_user.uid = ?", u.ID). | |||||
| GroupBy("repository.id").Find(&ids) | |||||
| } | |||||
| // GetAccessRepoIDs returns all repositories IDs where user's or user is a team member organizations | |||||
| func (u *User) GetAccessRepoIDs(units ...UnitType) ([]int64, error) { | |||||
| ids, err := u.GetRepositoryIDs(units...) | |||||
| if err != nil { | |||||
| return nil, err | |||||
| } | |||||
| ids2, err := u.GetOrgRepositoryIDs(units...) | |||||
| if err != nil { | |||||
| return nil, err | |||||
| } | |||||
| return append(ids, ids2...), nil | |||||
| } | } | ||||
| // GetMirrorRepositories returns mirror repositories that user owns, including private repositories. | // GetMirrorRepositories returns mirror repositories that user owns, including private repositories. | ||||
| @@ -275,6 +275,28 @@ func BenchmarkHashPassword(b *testing.B) { | |||||
| } | } | ||||
| } | } | ||||
| func TestGetOrgRepositoryIDs(t *testing.T) { | |||||
| assert.NoError(t, PrepareTestDatabase()) | |||||
| user2 := AssertExistsAndLoadBean(t, &User{ID: 2}).(*User) | |||||
| user4 := AssertExistsAndLoadBean(t, &User{ID: 4}).(*User) | |||||
| user5 := AssertExistsAndLoadBean(t, &User{ID: 5}).(*User) | |||||
| accessibleRepos, err := user2.GetOrgRepositoryIDs() | |||||
| 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 = user4.GetOrgRepositoryIDs() | |||||
| 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 = user5.GetOrgRepositoryIDs() | |||||
| assert.NoError(t, err) | |||||
| // User 5's team has no access to any repo | |||||
| assert.Len(t, accessibleRepos, 0) | |||||
| } | |||||
| func TestNewGitSig(t *testing.T) { | func TestNewGitSig(t *testing.T) { | ||||
| users := make([]*User, 0, 20) | users := make([]*User, 0, 20) | ||||
| sess := x.NewSession() | sess := x.NewSession() | ||||
| @@ -14,11 +14,13 @@ import ( | |||||
| "code.gitea.io/gitea/models" | "code.gitea.io/gitea/models" | ||||
| "code.gitea.io/gitea/modules/base" | "code.gitea.io/gitea/modules/base" | ||||
| "code.gitea.io/gitea/modules/context" | "code.gitea.io/gitea/modules/context" | ||||
| "code.gitea.io/gitea/modules/log" | |||||
| "code.gitea.io/gitea/modules/setting" | "code.gitea.io/gitea/modules/setting" | ||||
| "code.gitea.io/gitea/modules/util" | "code.gitea.io/gitea/modules/util" | ||||
| "github.com/keybase/go-crypto/openpgp" | "github.com/keybase/go-crypto/openpgp" | ||||
| "github.com/keybase/go-crypto/openpgp/armor" | "github.com/keybase/go-crypto/openpgp/armor" | ||||
| "github.com/unknwon/com" | |||||
| ) | ) | ||||
| const ( | const ( | ||||
| @@ -150,24 +152,6 @@ func Dashboard(ctx *context.Context) { | |||||
| // Issues render the user issues page | // Issues render the user issues page | ||||
| func Issues(ctx *context.Context) { | func Issues(ctx *context.Context) { | ||||
| isPullList := ctx.Params(":type") == "pulls" | isPullList := ctx.Params(":type") == "pulls" | ||||
| repoID := ctx.QueryInt64("repo") | |||||
| if repoID > 0 { | |||||
| repo, err := models.GetRepositoryByID(repoID) | |||||
| if err != nil { | |||||
| ctx.ServerError("GetRepositoryByID", err) | |||||
| return | |||||
| } | |||||
| perm, err := models.GetUserRepoPermission(repo, ctx.User) | |||||
| if err != nil { | |||||
| ctx.ServerError("GetUserRepoPermission", err) | |||||
| return | |||||
| } | |||||
| if !perm.CanReadIssuesOrPulls(isPullList) { | |||||
| ctx.NotFound("Repository does not exist or you have no permission", nil) | |||||
| return | |||||
| } | |||||
| } | |||||
| if isPullList { | if isPullList { | ||||
| ctx.Data["Title"] = ctx.Tr("pull_requests") | ctx.Data["Title"] = ctx.Tr("pull_requests") | ||||
| ctx.Data["PageIsPulls"] = true | ctx.Data["PageIsPulls"] = true | ||||
| @@ -210,32 +194,58 @@ func Issues(ctx *context.Context) { | |||||
| page = 1 | page = 1 | ||||
| } | } | ||||
| var ( | |||||
| isShowClosed = ctx.Query("state") == "closed" | |||||
| err error | |||||
| opts = &models.IssuesOptions{ | |||||
| IsClosed: util.OptionalBoolOf(isShowClosed), | |||||
| IsPull: util.OptionalBoolOf(isPullList), | |||||
| SortType: sortType, | |||||
| } | |||||
| ) | |||||
| repoID := ctx.QueryInt64("repo") | |||||
| isShowClosed := ctx.Query("state") == "closed" | |||||
| // Get repositories. | // Get repositories. | ||||
| if repoID > 0 { | |||||
| opts.RepoIDs = []int64{repoID} | |||||
| var err error | |||||
| var userRepoIDs []int64 | |||||
| if ctxUser.IsOrganization() { | |||||
| env, err := ctxUser.AccessibleReposEnv(ctx.User.ID) | |||||
| if err != nil { | |||||
| ctx.ServerError("AccessibleReposEnv", err) | |||||
| return | |||||
| } | |||||
| userRepoIDs, err = env.RepoIDs(1, ctxUser.NumRepos) | |||||
| if err != nil { | |||||
| ctx.ServerError("env.RepoIDs", err) | |||||
| return | |||||
| } | |||||
| } else { | } else { | ||||
| unitType := models.UnitTypeIssues | unitType := models.UnitTypeIssues | ||||
| if isPullList { | if isPullList { | ||||
| unitType = models.UnitTypePullRequests | unitType = models.UnitTypePullRequests | ||||
| } | } | ||||
| if ctxUser.IsOrganization() { | |||||
| opts.RepoSubQuery = ctxUser.OrgUnitRepositoriesSubQuery(ctx.User.ID, unitType) | |||||
| } else { | |||||
| opts.RepoSubQuery = ctxUser.UnitRepositoriesSubQuery(unitType) | |||||
| userRepoIDs, err = ctxUser.GetAccessRepoIDs(unitType) | |||||
| if err != nil { | |||||
| ctx.ServerError("ctxUser.GetAccessRepoIDs", err) | |||||
| return | |||||
| } | } | ||||
| } | } | ||||
| if len(userRepoIDs) == 0 { | |||||
| userRepoIDs = []int64{-1} | |||||
| } | |||||
| opts := &models.IssuesOptions{ | |||||
| IsClosed: util.OptionalBoolOf(isShowClosed), | |||||
| IsPull: util.OptionalBoolOf(isPullList), | |||||
| SortType: sortType, | |||||
| } | |||||
| if repoID > 0 { | |||||
| opts.RepoIDs = []int64{repoID} | |||||
| } | |||||
| switch filterMode { | switch filterMode { | ||||
| case models.FilterModeAll: | |||||
| if repoID > 0 { | |||||
| if !com.IsSliceContainsInt64(userRepoIDs, repoID) { | |||||
| // force an empty result | |||||
| opts.RepoIDs = []int64{-1} | |||||
| } | |||||
| } else { | |||||
| opts.RepoIDs = userRepoIDs | |||||
| } | |||||
| case models.FilterModeAssign: | case models.FilterModeAssign: | ||||
| opts.AssigneeID = ctxUser.ID | opts.AssigneeID = ctxUser.ID | ||||
| case models.FilterModeCreate: | case models.FilterModeCreate: | ||||
| @@ -244,6 +254,14 @@ func Issues(ctx *context.Context) { | |||||
| opts.MentionedID = ctxUser.ID | opts.MentionedID = ctxUser.ID | ||||
| } | } | ||||
| counts, err := models.CountIssuesByRepo(opts) | |||||
| if err != nil { | |||||
| ctx.ServerError("CountIssuesByRepo", err) | |||||
| return | |||||
| } | |||||
| opts.Page = page | |||||
| opts.PageSize = setting.UI.IssuePagingNum | |||||
| var labelIDs []int64 | var labelIDs []int64 | ||||
| selectLabels := ctx.Query("labels") | selectLabels := ctx.Query("labels") | ||||
| if len(selectLabels) > 0 && selectLabels != "0" { | if len(selectLabels) > 0 && selectLabels != "0" { | ||||
| @@ -255,15 +273,6 @@ func Issues(ctx *context.Context) { | |||||
| } | } | ||||
| opts.LabelIDs = labelIDs | opts.LabelIDs = labelIDs | ||||
| counts, err := models.CountIssuesByRepo(opts) | |||||
| if err != nil { | |||||
| ctx.ServerError("CountIssuesByRepo", err) | |||||
| return | |||||
| } | |||||
| opts.Page = page | |||||
| opts.PageSize = setting.UI.IssuePagingNum | |||||
| issues, err := models.Issues(opts) | issues, err := models.Issues(opts) | ||||
| if err != nil { | if err != nil { | ||||
| ctx.ServerError("Issues", err) | ctx.ServerError("Issues", err) | ||||
| @@ -280,6 +289,41 @@ func Issues(ctx *context.Context) { | |||||
| showReposMap[repoID] = repo | showReposMap[repoID] = repo | ||||
| } | } | ||||
| if repoID > 0 { | |||||
| if _, ok := showReposMap[repoID]; !ok { | |||||
| repo, err := models.GetRepositoryByID(repoID) | |||||
| if models.IsErrRepoNotExist(err) { | |||||
| ctx.NotFound("GetRepositoryByID", err) | |||||
| return | |||||
| } else if err != nil { | |||||
| ctx.ServerError("GetRepositoryByID", fmt.Errorf("[%d]%v", repoID, err)) | |||||
| return | |||||
| } | |||||
| showReposMap[repoID] = repo | |||||
| } | |||||
| repo := showReposMap[repoID] | |||||
| // Check if user has access to given repository. | |||||
| perm, err := models.GetUserRepoPermission(repo, ctxUser) | |||||
| if err != nil { | |||||
| ctx.ServerError("GetUserRepoPermission", fmt.Errorf("[%d]%v", repoID, err)) | |||||
| return | |||||
| } | |||||
| if !perm.CanRead(models.UnitTypeIssues) { | |||||
| if log.IsTrace() { | |||||
| log.Trace("Permission Denied: User %-v cannot read %-v of repo %-v\n"+ | |||||
| "User in repo has Permissions: %-+v", | |||||
| ctxUser, | |||||
| models.UnitTypeIssues, | |||||
| repo, | |||||
| perm) | |||||
| } | |||||
| ctx.Status(404) | |||||
| return | |||||
| } | |||||
| } | |||||
| showRepos := models.RepositoryListOfMap(showReposMap) | showRepos := models.RepositoryListOfMap(showReposMap) | ||||
| sort.Sort(showRepos) | sort.Sort(showRepos) | ||||
| if err = showRepos.LoadAttributes(); err != nil { | if err = showRepos.LoadAttributes(); err != nil { | ||||
| @@ -297,12 +341,12 @@ func Issues(ctx *context.Context) { | |||||
| } | } | ||||
| issueStats, err := models.GetUserIssueStats(models.UserIssueStatsOptions{ | issueStats, err := models.GetUserIssueStats(models.UserIssueStatsOptions{ | ||||
| UserID: ctxUser.ID, | |||||
| RepoID: repoID, | |||||
| RepoSubQuery: opts.RepoSubQuery, | |||||
| FilterMode: filterMode, | |||||
| IsPull: isPullList, | |||||
| IsClosed: isShowClosed, | |||||
| UserID: ctxUser.ID, | |||||
| RepoID: repoID, | |||||
| UserRepoIDs: userRepoIDs, | |||||
| FilterMode: filterMode, | |||||
| IsPull: isPullList, | |||||
| IsClosed: isShowClosed, | |||||
| }) | }) | ||||
| if err != nil { | if err != nil { | ||||
| ctx.ServerError("GetUserIssueStats", err) | ctx.ServerError("GetUserIssueStats", err) | ||||