* Fix milestones too many SQL variables bug * Fix test * Don't display repositories with no milestone and fix tests * Remove unused code and add some commentstags/v1.21.12.1
| @@ -525,10 +525,12 @@ func DeleteMilestoneByRepoID(repoID, id int64) error { | |||||
| return sess.Commit() | return sess.Commit() | ||||
| } | } | ||||
| // CountMilestonesByRepoIDs map from repoIDs to number of milestones matching the options` | |||||
| func CountMilestonesByRepoIDs(repoIDs []int64, isClosed bool) (map[int64]int64, error) { | |||||
| // CountMilestones map from repo conditions to number of milestones matching the options` | |||||
| func CountMilestones(repoCond builder.Cond, isClosed bool) (map[int64]int64, error) { | |||||
| sess := x.Where("is_closed = ?", isClosed) | sess := x.Where("is_closed = ?", isClosed) | ||||
| sess.In("repo_id", repoIDs) | |||||
| if repoCond.IsValid() { | |||||
| sess.In("repo_id", builder.Select("id").From("repository").Where(repoCond)) | |||||
| } | |||||
| countsSlice := make([]*struct { | countsSlice := make([]*struct { | ||||
| RepoID int64 | RepoID int64 | ||||
| @@ -548,11 +550,21 @@ func CountMilestonesByRepoIDs(repoIDs []int64, isClosed bool) (map[int64]int64, | |||||
| return countMap, nil | return countMap, nil | ||||
| } | } | ||||
| // GetMilestonesByRepoIDs returns a list of milestones of given repositories and status. | |||||
| func GetMilestonesByRepoIDs(repoIDs []int64, page int, isClosed bool, sortType string) (MilestoneList, error) { | |||||
| // CountMilestonesByRepoIDs map from repoIDs to number of milestones matching the options` | |||||
| func CountMilestonesByRepoIDs(repoIDs []int64, isClosed bool) (map[int64]int64, error) { | |||||
| return CountMilestones( | |||||
| builder.In("repo_id", repoIDs), | |||||
| isClosed, | |||||
| ) | |||||
| } | |||||
| // SearchMilestones search milestones | |||||
| func SearchMilestones(repoCond builder.Cond, page int, isClosed bool, sortType string) (MilestoneList, error) { | |||||
| miles := make([]*Milestone, 0, setting.UI.IssuePagingNum) | miles := make([]*Milestone, 0, setting.UI.IssuePagingNum) | ||||
| sess := x.Where("is_closed = ?", isClosed) | sess := x.Where("is_closed = ?", isClosed) | ||||
| sess.In("repo_id", repoIDs) | |||||
| if repoCond.IsValid() { | |||||
| sess.In("repo_id", builder.Select("id").From("repository").Where(repoCond)) | |||||
| } | |||||
| if page > 0 { | if page > 0 { | ||||
| sess = sess.Limit(setting.UI.IssuePagingNum, (page-1)*setting.UI.IssuePagingNum) | sess = sess.Limit(setting.UI.IssuePagingNum, (page-1)*setting.UI.IssuePagingNum) | ||||
| } | } | ||||
| @@ -574,25 +586,45 @@ func GetMilestonesByRepoIDs(repoIDs []int64, page int, isClosed bool, sortType s | |||||
| return miles, sess.Find(&miles) | return miles, sess.Find(&miles) | ||||
| } | } | ||||
| // GetMilestonesByRepoIDs returns a list of milestones of given repositories and status. | |||||
| func GetMilestonesByRepoIDs(repoIDs []int64, page int, isClosed bool, sortType string) (MilestoneList, error) { | |||||
| return SearchMilestones( | |||||
| builder.In("repo_id", repoIDs), | |||||
| page, | |||||
| isClosed, | |||||
| sortType, | |||||
| ) | |||||
| } | |||||
| // MilestonesStats represents milestone statistic information. | // MilestonesStats represents milestone statistic information. | ||||
| type MilestonesStats struct { | type MilestonesStats struct { | ||||
| OpenCount, ClosedCount int64 | OpenCount, ClosedCount int64 | ||||
| } | } | ||||
| // Total returns the total counts of milestones | |||||
| func (m MilestonesStats) Total() int64 { | |||||
| return m.OpenCount + m.ClosedCount | |||||
| } | |||||
| // GetMilestonesStats returns milestone statistic information for dashboard by given conditions. | // GetMilestonesStats returns milestone statistic information for dashboard by given conditions. | ||||
| func GetMilestonesStats(userRepoIDs []int64) (*MilestonesStats, error) { | |||||
| func GetMilestonesStats(repoCond builder.Cond) (*MilestonesStats, error) { | |||||
| var err error | var err error | ||||
| stats := &MilestonesStats{} | stats := &MilestonesStats{} | ||||
| stats.OpenCount, err = x.Where("is_closed = ?", false). | |||||
| And(builder.In("repo_id", userRepoIDs)). | |||||
| Count(new(Milestone)) | |||||
| sess := x.Where("is_closed = ?", false) | |||||
| if repoCond.IsValid() { | |||||
| sess.And(builder.In("repo_id", builder.Select("id").From("repository").Where(repoCond))) | |||||
| } | |||||
| stats.OpenCount, err = sess.Count(new(Milestone)) | |||||
| if err != nil { | if err != nil { | ||||
| return nil, err | return nil, err | ||||
| } | } | ||||
| stats.ClosedCount, err = x.Where("is_closed = ?", true). | |||||
| And(builder.In("repo_id", userRepoIDs)). | |||||
| Count(new(Milestone)) | |||||
| sess = x.Where("is_closed = ?", true) | |||||
| if repoCond.IsValid() { | |||||
| sess.And(builder.In("repo_id", builder.Select("id").From("repository").Where(repoCond))) | |||||
| } | |||||
| stats.ClosedCount, err = sess.Count(new(Milestone)) | |||||
| if err != nil { | if err != nil { | ||||
| return nil, err | return nil, err | ||||
| } | } | ||||
| @@ -11,6 +11,7 @@ import ( | |||||
| api "code.gitea.io/gitea/modules/structs" | api "code.gitea.io/gitea/modules/structs" | ||||
| "code.gitea.io/gitea/modules/timeutil" | "code.gitea.io/gitea/modules/timeutil" | ||||
| "xorm.io/builder" | |||||
| "github.com/stretchr/testify/assert" | "github.com/stretchr/testify/assert" | ||||
| ) | ) | ||||
| @@ -370,7 +371,7 @@ func TestGetMilestonesStats(t *testing.T) { | |||||
| repo1 := AssertExistsAndLoadBean(t, &Repository{ID: 1}).(*Repository) | repo1 := AssertExistsAndLoadBean(t, &Repository{ID: 1}).(*Repository) | ||||
| repo2 := AssertExistsAndLoadBean(t, &Repository{ID: 2}).(*Repository) | repo2 := AssertExistsAndLoadBean(t, &Repository{ID: 2}).(*Repository) | ||||
| milestoneStats, err := GetMilestonesStats([]int64{repo1.ID, repo2.ID}) | |||||
| milestoneStats, err := GetMilestonesStats(builder.In("repo_id", []int64{repo1.ID, repo2.ID})) | |||||
| assert.NoError(t, err) | assert.NoError(t, err) | ||||
| assert.EqualValues(t, repo1.NumOpenMilestones+repo2.NumOpenMilestones, milestoneStats.OpenCount) | assert.EqualValues(t, repo1.NumOpenMilestones+repo2.NumOpenMilestones, milestoneStats.OpenCount) | ||||
| assert.EqualValues(t, repo1.NumClosedMilestones+repo2.NumClosedMilestones, milestoneStats.ClosedCount) | assert.EqualValues(t, repo1.NumClosedMilestones+repo2.NumClosedMilestones, milestoneStats.ClosedCount) | ||||
| @@ -163,6 +163,10 @@ type SearchRepoOptions struct { | |||||
| TopicOnly bool | TopicOnly bool | ||||
| // include description in keyword search | // include description in keyword search | ||||
| IncludeDescription bool | IncludeDescription bool | ||||
| // None -> include has milestones AND has no milestone | |||||
| // True -> include just has milestones | |||||
| // False -> include just has no milestone | |||||
| HasMilestones util.OptionalBool | |||||
| } | } | ||||
| //SearchOrderBy is used to sort the result | //SearchOrderBy is used to sort the result | ||||
| @@ -294,6 +298,14 @@ func SearchRepositoryCondition(opts *SearchRepoOptions) builder.Cond { | |||||
| if opts.Actor != nil && opts.Actor.IsRestricted { | if opts.Actor != nil && opts.Actor.IsRestricted { | ||||
| cond = cond.And(accessibleRepositoryCondition(opts.Actor)) | cond = cond.And(accessibleRepositoryCondition(opts.Actor)) | ||||
| } | } | ||||
| switch opts.HasMilestones { | |||||
| case util.OptionalBoolTrue: | |||||
| cond = cond.And(builder.Gt{"num_milestones": 0}) | |||||
| case util.OptionalBoolFalse: | |||||
| cond = cond.And(builder.Eq{"num_milestones": 0}.Or(builder.IsNull{"num_milestones"})) | |||||
| } | |||||
| return cond | return cond | ||||
| } | } | ||||
| @@ -301,7 +313,11 @@ func SearchRepositoryCondition(opts *SearchRepoOptions) builder.Cond { | |||||
| // it returns results in given range and number of total results. | // it returns results in given range and number of total results. | ||||
| func SearchRepository(opts *SearchRepoOptions) (RepositoryList, int64, error) { | func SearchRepository(opts *SearchRepoOptions) (RepositoryList, int64, error) { | ||||
| cond := SearchRepositoryCondition(opts) | cond := SearchRepositoryCondition(opts) | ||||
| return SearchRepositoryByCondition(opts, cond, true) | |||||
| } | |||||
| // SearchRepositoryByCondition search repositories by condition | |||||
| func SearchRepositoryByCondition(opts *SearchRepoOptions, cond builder.Cond, loadAttributes bool) (RepositoryList, int64, error) { | |||||
| if opts.Page <= 0 { | if opts.Page <= 0 { | ||||
| opts.Page = 1 | opts.Page = 1 | ||||
| } | } | ||||
| @@ -326,16 +342,18 @@ func SearchRepository(opts *SearchRepoOptions) (RepositoryList, int64, error) { | |||||
| } | } | ||||
| repos := make(RepositoryList, 0, opts.PageSize) | repos := make(RepositoryList, 0, opts.PageSize) | ||||
| if err = sess. | |||||
| Where(cond). | |||||
| OrderBy(opts.OrderBy.String()). | |||||
| Limit(opts.PageSize, (opts.Page-1)*opts.PageSize). | |||||
| Find(&repos); err != nil { | |||||
| sess.Where(cond).OrderBy(opts.OrderBy.String()) | |||||
| if opts.PageSize > 0 { | |||||
| sess.Limit(opts.PageSize, (opts.Page-1)*opts.PageSize) | |||||
| } | |||||
| if err = sess.Find(&repos); err != nil { | |||||
| return nil, 0, fmt.Errorf("Repo: %v", err) | return nil, 0, fmt.Errorf("Repo: %v", err) | ||||
| } | } | ||||
| if err = repos.loadAttributes(sess); err != nil { | |||||
| return nil, 0, fmt.Errorf("LoadAttributes: %v", err) | |||||
| if loadAttributes { | |||||
| if err = repos.loadAttributes(sess); err != nil { | |||||
| return nil, 0, fmt.Errorf("LoadAttributes: %v", err) | |||||
| } | |||||
| } | } | ||||
| return repos, count, nil | return repos, count, nil | ||||
| @@ -25,7 +25,7 @@ import ( | |||||
| "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" | |||||
| "xorm.io/builder" | |||||
| ) | ) | ||||
| const ( | const ( | ||||
| @@ -173,135 +173,114 @@ func Milestones(ctx *context.Context) { | |||||
| return | return | ||||
| } | } | ||||
| sortType := ctx.Query("sort") | |||||
| page := ctx.QueryInt("page") | |||||
| var ( | |||||
| repoOpts = models.SearchRepoOptions{ | |||||
| Actor: ctxUser, | |||||
| OwnerID: ctxUser.ID, | |||||
| Private: true, | |||||
| AllPublic: false, // Include also all public repositories of users and public organisations | |||||
| AllLimited: false, // Include also all public repositories of limited organisations | |||||
| HasMilestones: util.OptionalBoolTrue, // Just needs display repos has milestones | |||||
| } | |||||
| userRepoCond = models.SearchRepositoryCondition(&repoOpts) // all repo condition user could visit | |||||
| repoCond = userRepoCond | |||||
| repoIDs []int64 | |||||
| reposQuery = ctx.Query("repos") | |||||
| isShowClosed = ctx.Query("state") == "closed" | |||||
| sortType = ctx.Query("sort") | |||||
| page = ctx.QueryInt("page") | |||||
| ) | |||||
| if page <= 1 { | if page <= 1 { | ||||
| page = 1 | page = 1 | ||||
| } | } | ||||
| reposQuery := ctx.Query("repos") | |||||
| isShowClosed := ctx.Query("state") == "closed" | |||||
| // Get repositories. | |||||
| 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 | |||||
| } | |||||
| userRepoIDs, err = models.FilterOutRepoIdsWithoutUnitAccess(ctx.User, userRepoIDs, models.UnitTypeIssues, models.UnitTypePullRequests) | |||||
| if err != nil { | |||||
| ctx.ServerError("FilterOutRepoIdsWithoutUnitAccess", err) | |||||
| return | |||||
| } | |||||
| } else { | |||||
| userRepoIDs, err = ctxUser.GetAccessRepoIDs(models.UnitTypeIssues, models.UnitTypePullRequests) | |||||
| if err != nil { | |||||
| ctx.ServerError("ctxUser.GetAccessRepoIDs", err) | |||||
| return | |||||
| } | |||||
| } | |||||
| if len(userRepoIDs) == 0 { | |||||
| userRepoIDs = []int64{-1} | |||||
| } | |||||
| var repoIDs []int64 | |||||
| if len(reposQuery) != 0 { | if len(reposQuery) != 0 { | ||||
| if issueReposQueryPattern.MatchString(reposQuery) { | if issueReposQueryPattern.MatchString(reposQuery) { | ||||
| // remove "[" and "]" from string | // remove "[" and "]" from string | ||||
| reposQuery = reposQuery[1 : len(reposQuery)-1] | reposQuery = reposQuery[1 : len(reposQuery)-1] | ||||
| //for each ID (delimiter ",") add to int to repoIDs | //for each ID (delimiter ",") add to int to repoIDs | ||||
| reposSet := false | |||||
| for _, rID := range strings.Split(reposQuery, ",") { | for _, rID := range strings.Split(reposQuery, ",") { | ||||
| // Ensure nonempty string entries | // Ensure nonempty string entries | ||||
| if rID != "" && rID != "0" { | if rID != "" && rID != "0" { | ||||
| reposSet = true | |||||
| rIDint64, err := strconv.ParseInt(rID, 10, 64) | rIDint64, err := strconv.ParseInt(rID, 10, 64) | ||||
| // If the repo id specified by query is not parseable or not accessible by user, just ignore it. | // If the repo id specified by query is not parseable or not accessible by user, just ignore it. | ||||
| if err == nil && com.IsSliceContainsInt64(userRepoIDs, rIDint64) { | |||||
| if err == nil { | |||||
| repoIDs = append(repoIDs, rIDint64) | repoIDs = append(repoIDs, rIDint64) | ||||
| } | } | ||||
| } | } | ||||
| } | } | ||||
| if reposSet && len(repoIDs) == 0 { | |||||
| // force an empty result | |||||
| repoIDs = []int64{-1} | |||||
| if len(repoIDs) > 0 { | |||||
| // Don't just let repoCond = builder.In("id", repoIDs) because user may has no permission on repoIDs | |||||
| // But the original repoCond has a limitation | |||||
| repoCond = repoCond.And(builder.In("id", repoIDs)) | |||||
| } | } | ||||
| } else { | } else { | ||||
| log.Warn("issueReposQueryPattern not match with query") | log.Warn("issueReposQueryPattern not match with query") | ||||
| } | } | ||||
| } | } | ||||
| if len(repoIDs) == 0 { | |||||
| repoIDs = userRepoIDs | |||||
| } | |||||
| counts, err := models.CountMilestonesByRepoIDs(userRepoIDs, isShowClosed) | |||||
| counts, err := models.CountMilestones(userRepoCond, isShowClosed) | |||||
| if err != nil { | if err != nil { | ||||
| ctx.ServerError("CountMilestonesByRepoIDs", err) | ctx.ServerError("CountMilestonesByRepoIDs", err) | ||||
| return | return | ||||
| } | } | ||||
| milestones, err := models.GetMilestonesByRepoIDs(repoIDs, page, isShowClosed, sortType) | |||||
| milestones, err := models.SearchMilestones(repoCond, page, isShowClosed, sortType) | |||||
| if err != nil { | if err != nil { | ||||
| ctx.ServerError("GetMilestonesByRepoIDs", err) | ctx.ServerError("GetMilestonesByRepoIDs", err) | ||||
| return | return | ||||
| } | } | ||||
| showReposMap := make(map[int64]*models.Repository, len(counts)) | |||||
| for rID := range counts { | |||||
| if rID == -1 { | |||||
| break | |||||
| } | |||||
| repo, err := models.GetRepositoryByID(rID) | |||||
| if err != nil { | |||||
| if models.IsErrRepoNotExist(err) { | |||||
| ctx.NotFound("GetRepositoryByID", err) | |||||
| return | |||||
| } else if err != nil { | |||||
| ctx.ServerError("GetRepositoryByID", fmt.Errorf("[%d]%v", rID, err)) | |||||
| return | |||||
| } | |||||
| } | |||||
| showReposMap[rID] = repo | |||||
| } | |||||
| showRepos := models.RepositoryListOfMap(showReposMap) | |||||
| sort.Sort(showRepos) | |||||
| if err = showRepos.LoadAttributes(); err != nil { | |||||
| ctx.ServerError("LoadAttributes", err) | |||||
| showRepos, _, err := models.SearchRepositoryByCondition(&repoOpts, userRepoCond, false) | |||||
| if err != nil { | |||||
| ctx.ServerError("SearchRepositoryByCondition", err) | |||||
| return | return | ||||
| } | } | ||||
| sort.Sort(showRepos) | |||||
| for i := 0; i < len(milestones); { | |||||
| for _, repo := range showRepos { | |||||
| if milestones[i].RepoID == repo.ID { | |||||
| milestones[i].Repo = repo | |||||
| break | |||||
| } | |||||
| } | |||||
| if milestones[i].Repo == nil { | |||||
| log.Warn("Cannot find milestone %d 's repository %d", milestones[i].ID, milestones[i].RepoID) | |||||
| milestones = append(milestones[:i], milestones[i+1:]...) | |||||
| continue | |||||
| } | |||||
| for _, m := range milestones { | |||||
| m.Repo = showReposMap[m.RepoID] | |||||
| m.RenderedContent = string(markdown.Render([]byte(m.Content), m.Repo.Link(), m.Repo.ComposeMetas())) | |||||
| if m.Repo.IsTimetrackerEnabled() { | |||||
| err := m.LoadTotalTrackedTime() | |||||
| milestones[i].RenderedContent = string(markdown.Render([]byte(milestones[i].Content), milestones[i].Repo.Link(), milestones[i].Repo.ComposeMetas())) | |||||
| if milestones[i].Repo.IsTimetrackerEnabled() { | |||||
| err := milestones[i].LoadTotalTrackedTime() | |||||
| if err != nil { | if err != nil { | ||||
| ctx.ServerError("LoadTotalTrackedTime", err) | ctx.ServerError("LoadTotalTrackedTime", err) | ||||
| return | return | ||||
| } | } | ||||
| } | } | ||||
| i++ | |||||
| } | } | ||||
| milestoneStats, err := models.GetMilestonesStats(repoIDs) | |||||
| milestoneStats, err := models.GetMilestonesStats(repoCond) | |||||
| if err != nil { | if err != nil { | ||||
| ctx.ServerError("GetMilestoneStats", err) | ctx.ServerError("GetMilestoneStats", err) | ||||
| return | return | ||||
| } | } | ||||
| totalMilestoneStats, err := models.GetMilestonesStats(userRepoIDs) | |||||
| if err != nil { | |||||
| ctx.ServerError("GetMilestoneStats", err) | |||||
| return | |||||
| var totalMilestoneStats *models.MilestonesStats | |||||
| if len(repoIDs) == 0 { | |||||
| totalMilestoneStats = milestoneStats | |||||
| } else { | |||||
| totalMilestoneStats, err = models.GetMilestonesStats(userRepoCond) | |||||
| if err != nil { | |||||
| ctx.ServerError("GetMilestoneStats", err) | |||||
| return | |||||
| } | |||||
| } | } | ||||
| var pagerCount int | var pagerCount int | ||||
| @@ -320,7 +299,7 @@ func Milestones(ctx *context.Context) { | |||||
| ctx.Data["Counts"] = counts | ctx.Data["Counts"] = counts | ||||
| ctx.Data["MilestoneStats"] = milestoneStats | ctx.Data["MilestoneStats"] = milestoneStats | ||||
| ctx.Data["SortType"] = sortType | ctx.Data["SortType"] = sortType | ||||
| if len(repoIDs) != len(userRepoIDs) { | |||||
| if milestoneStats.Total() != totalMilestoneStats.Total() { | |||||
| ctx.Data["RepoIDs"] = repoIDs | ctx.Data["RepoIDs"] = repoIDs | ||||
| } | } | ||||
| ctx.Data["IsShowClosed"] = isShowClosed | ctx.Data["IsShowClosed"] = isShowClosed | ||||
| @@ -48,7 +48,7 @@ func TestMilestones(t *testing.T) { | |||||
| assert.EqualValues(t, "furthestduedate", ctx.Data["SortType"]) | assert.EqualValues(t, "furthestduedate", ctx.Data["SortType"]) | ||||
| assert.EqualValues(t, 1, ctx.Data["Total"]) | assert.EqualValues(t, 1, ctx.Data["Total"]) | ||||
| assert.Len(t, ctx.Data["Milestones"], 1) | assert.Len(t, ctx.Data["Milestones"], 1) | ||||
| assert.Len(t, ctx.Data["Repos"], 1) | |||||
| assert.Len(t, ctx.Data["Repos"], 2) // both repo 42 and 1 have milestones and both are owned by user 2 | |||||
| } | } | ||||
| func TestMilestonesForSpecificRepo(t *testing.T) { | func TestMilestonesForSpecificRepo(t *testing.T) { | ||||
| @@ -68,5 +68,5 @@ func TestMilestonesForSpecificRepo(t *testing.T) { | |||||
| assert.EqualValues(t, "furthestduedate", ctx.Data["SortType"]) | assert.EqualValues(t, "furthestduedate", ctx.Data["SortType"]) | ||||
| assert.EqualValues(t, 1, ctx.Data["Total"]) | assert.EqualValues(t, 1, ctx.Data["Total"]) | ||||
| assert.Len(t, ctx.Data["Milestones"], 1) | assert.Len(t, ctx.Data["Milestones"], 1) | ||||
| assert.Len(t, ctx.Data["Repos"], 1) | |||||
| assert.Len(t, ctx.Data["Repos"], 2) // both repo 42 and 1 have milestones and both are owned by user 2 | |||||
| } | } | ||||