Some refactors for issues stats (#24793)

This PR

- [x] Move some functions from `issues.go` to `issue_stats.go` and
`issue_label.go`
- [x] Remove duplicated issue options `UserIssueStatsOption` to keep
only one `IssuesOptions`
This commit is contained in:
2023-05-19 22:17:48 +08:00
committed by GitHub
parent c757765a9e
commit 38cf43d060
12 changed files with 948 additions and 948 deletions

View File

@@ -8,10 +8,8 @@ import (
"context"
"fmt"
"regexp"
"sort"
"code.gitea.io/gitea/models/db"
access_model "code.gitea.io/gitea/models/perm/access"
project_model "code.gitea.io/gitea/models/project"
repo_model "code.gitea.io/gitea/models/repo"
user_model "code.gitea.io/gitea/models/user"
@@ -212,17 +210,6 @@ func (issue *Issue) GetPullRequest() (pr *PullRequest, err error) {
return pr, err
}
// LoadLabels loads labels
func (issue *Issue) LoadLabels(ctx context.Context) (err error) {
if issue.Labels == nil && issue.ID != 0 {
issue.Labels, err = GetLabelsByIssueID(ctx, issue.ID)
if err != nil {
return fmt.Errorf("getLabelsByIssueID [%d]: %w", issue.ID, err)
}
}
return nil
}
// LoadPoster loads poster
func (issue *Issue) LoadPoster(ctx context.Context) (err error) {
if issue.Poster == nil && issue.PosterID != 0 {
@@ -459,175 +446,6 @@ func (issue *Issue) IsPoster(uid int64) bool {
return issue.OriginalAuthorID == 0 && issue.PosterID == uid
}
func (issue *Issue) getLabels(ctx context.Context) (err error) {
if len(issue.Labels) > 0 {
return nil
}
issue.Labels, err = GetLabelsByIssueID(ctx, issue.ID)
if err != nil {
return fmt.Errorf("getLabelsByIssueID: %w", err)
}
return nil
}
func clearIssueLabels(ctx context.Context, issue *Issue, doer *user_model.User) (err error) {
if err = issue.getLabels(ctx); err != nil {
return fmt.Errorf("getLabels: %w", err)
}
for i := range issue.Labels {
if err = deleteIssueLabel(ctx, issue, issue.Labels[i], doer); err != nil {
return fmt.Errorf("removeLabel: %w", err)
}
}
return nil
}
// ClearIssueLabels removes all issue labels as the given user.
// Triggers appropriate WebHooks, if any.
func ClearIssueLabels(issue *Issue, doer *user_model.User) (err error) {
ctx, committer, err := db.TxContext(db.DefaultContext)
if err != nil {
return err
}
defer committer.Close()
if err := issue.LoadRepo(ctx); err != nil {
return err
} else if err = issue.LoadPullRequest(ctx); err != nil {
return err
}
perm, err := access_model.GetUserRepoPermission(ctx, issue.Repo, doer)
if err != nil {
return err
}
if !perm.CanWriteIssuesOrPulls(issue.IsPull) {
return ErrRepoLabelNotExist{}
}
if err = clearIssueLabels(ctx, issue, doer); err != nil {
return err
}
if err = committer.Commit(); err != nil {
return fmt.Errorf("Commit: %w", err)
}
return nil
}
type labelSorter []*Label
func (ts labelSorter) Len() int {
return len([]*Label(ts))
}
func (ts labelSorter) Less(i, j int) bool {
return []*Label(ts)[i].ID < []*Label(ts)[j].ID
}
func (ts labelSorter) Swap(i, j int) {
[]*Label(ts)[i], []*Label(ts)[j] = []*Label(ts)[j], []*Label(ts)[i]
}
// Ensure only one label of a given scope exists, with labels at the end of the
// array getting preference over earlier ones.
func RemoveDuplicateExclusiveLabels(labels []*Label) []*Label {
validLabels := make([]*Label, 0, len(labels))
for i, label := range labels {
scope := label.ExclusiveScope()
if scope != "" {
foundOther := false
for _, otherLabel := range labels[i+1:] {
if otherLabel.ExclusiveScope() == scope {
foundOther = true
break
}
}
if foundOther {
continue
}
}
validLabels = append(validLabels, label)
}
return validLabels
}
// ReplaceIssueLabels removes all current labels and add new labels to the issue.
// Triggers appropriate WebHooks, if any.
func ReplaceIssueLabels(issue *Issue, labels []*Label, doer *user_model.User) (err error) {
ctx, committer, err := db.TxContext(db.DefaultContext)
if err != nil {
return err
}
defer committer.Close()
if err = issue.LoadRepo(ctx); err != nil {
return err
}
if err = issue.LoadLabels(ctx); err != nil {
return err
}
labels = RemoveDuplicateExclusiveLabels(labels)
sort.Sort(labelSorter(labels))
sort.Sort(labelSorter(issue.Labels))
var toAdd, toRemove []*Label
addIndex, removeIndex := 0, 0
for addIndex < len(labels) && removeIndex < len(issue.Labels) {
addLabel := labels[addIndex]
removeLabel := issue.Labels[removeIndex]
if addLabel.ID == removeLabel.ID {
// Silently drop invalid labels
if removeLabel.RepoID != issue.RepoID && removeLabel.OrgID != issue.Repo.OwnerID {
toRemove = append(toRemove, removeLabel)
}
addIndex++
removeIndex++
} else if addLabel.ID < removeLabel.ID {
// Only add if the label is valid
if addLabel.RepoID == issue.RepoID || addLabel.OrgID == issue.Repo.OwnerID {
toAdd = append(toAdd, addLabel)
}
addIndex++
} else {
toRemove = append(toRemove, removeLabel)
removeIndex++
}
}
toAdd = append(toAdd, labels[addIndex:]...)
toRemove = append(toRemove, issue.Labels[removeIndex:]...)
if len(toAdd) > 0 {
if err = newIssueLabels(ctx, issue, toAdd, doer); err != nil {
return fmt.Errorf("addLabels: %w", err)
}
}
for _, l := range toRemove {
if err = deleteIssueLabel(ctx, issue, l, doer); err != nil {
return fmt.Errorf("removeLabel: %w", err)
}
}
issue.Labels = nil
if err = issue.LoadLabels(ctx); err != nil {
return err
}
return committer.Commit()
}
// GetTasks returns the amount of tasks in the issues content
func (issue *Issue) GetTasks() int {
return len(issueTasksPat.FindAllStringIndex(issue.Content, -1))
@@ -862,16 +680,6 @@ func (issue *Issue) GetExternalName() string { return issue.OriginalAuthor }
// GetExternalID ExternalUserRemappable interface
func (issue *Issue) GetExternalID() int64 { return issue.OriginalAuthorID }
// CountOrphanedIssues count issues without a repo
func CountOrphanedIssues(ctx context.Context) (int64, error) {
return db.GetEngine(ctx).
Table("issue").
Join("LEFT", "repository", "issue.repo_id=repository.id").
Where(builder.IsNull{"repository.id"}).
Select("COUNT(`issue`.`id`)").
Count()
}
// HasOriginalAuthor returns if an issue was migrated and has an original author.
func (issue *Issue) HasOriginalAuthor() bool {
return issue.OriginalAuthor != "" && issue.OriginalAuthorID != 0

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -17,6 +17,7 @@ import (
repo_model "code.gitea.io/gitea/models/repo"
"code.gitea.io/gitea/models/unittest"
user_model "code.gitea.io/gitea/models/user"
"code.gitea.io/gitea/modules/util"
"github.com/stretchr/testify/assert"
"xorm.io/builder"
@@ -204,14 +205,16 @@ func TestIssues(t *testing.T) {
func TestGetUserIssueStats(t *testing.T) {
assert.NoError(t, unittest.PrepareTestDatabase())
for _, test := range []struct {
Opts issues_model.UserIssueStatsOptions
FilterMode int
Opts issues_model.IssuesOptions
ExpectedIssueStats issues_model.IssueStats
}{
{
issues_model.UserIssueStatsOptions{
UserID: 1,
RepoIDs: []int64{1},
FilterMode: issues_model.FilterModeAll,
issues_model.FilterModeAll,
issues_model.IssuesOptions{
User: unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 1}),
RepoIDs: []int64{1},
IsPull: util.OptionalBoolFalse,
},
issues_model.IssueStats{
YourRepositoriesCount: 1, // 6
@@ -222,11 +225,12 @@ func TestGetUserIssueStats(t *testing.T) {
},
},
{
issues_model.UserIssueStatsOptions{
UserID: 1,
RepoIDs: []int64{1},
FilterMode: issues_model.FilterModeAll,
IsClosed: true,
issues_model.FilterModeAll,
issues_model.IssuesOptions{
User: unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 1}),
RepoIDs: []int64{1},
IsPull: util.OptionalBoolFalse,
IsClosed: util.OptionalBoolTrue,
},
issues_model.IssueStats{
YourRepositoriesCount: 1, // 6
@@ -237,9 +241,10 @@ func TestGetUserIssueStats(t *testing.T) {
},
},
{
issues_model.UserIssueStatsOptions{
UserID: 1,
FilterMode: issues_model.FilterModeAssign,
issues_model.FilterModeAssign,
issues_model.IssuesOptions{
User: unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 1}),
IsPull: util.OptionalBoolFalse,
},
issues_model.IssueStats{
YourRepositoriesCount: 1, // 6
@@ -250,9 +255,10 @@ func TestGetUserIssueStats(t *testing.T) {
},
},
{
issues_model.UserIssueStatsOptions{
UserID: 1,
FilterMode: issues_model.FilterModeCreate,
issues_model.FilterModeCreate,
issues_model.IssuesOptions{
User: unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 1}),
IsPull: util.OptionalBoolFalse,
},
issues_model.IssueStats{
YourRepositoriesCount: 1, // 6
@@ -263,9 +269,10 @@ func TestGetUserIssueStats(t *testing.T) {
},
},
{
issues_model.UserIssueStatsOptions{
UserID: 1,
FilterMode: issues_model.FilterModeMention,
issues_model.FilterModeMention,
issues_model.IssuesOptions{
User: unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 1}),
IsPull: util.OptionalBoolFalse,
},
issues_model.IssueStats{
YourRepositoriesCount: 1, // 6
@@ -277,10 +284,11 @@ func TestGetUserIssueStats(t *testing.T) {
},
},
{
issues_model.UserIssueStatsOptions{
UserID: 1,
FilterMode: issues_model.FilterModeCreate,
IssueIDs: []int64{1},
issues_model.FilterModeCreate,
issues_model.IssuesOptions{
User: unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 1}),
IssueIDs: []int64{1},
IsPull: util.OptionalBoolFalse,
},
issues_model.IssueStats{
YourRepositoriesCount: 1, // 1
@@ -291,11 +299,12 @@ func TestGetUserIssueStats(t *testing.T) {
},
},
{
issues_model.UserIssueStatsOptions{
UserID: 2,
Org: unittest.AssertExistsAndLoadBean(t, &organization.Organization{ID: 3}),
Team: unittest.AssertExistsAndLoadBean(t, &organization.Team{ID: 7}),
FilterMode: issues_model.FilterModeAll,
issues_model.FilterModeAll,
issues_model.IssuesOptions{
User: unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2}),
Org: unittest.AssertExistsAndLoadBean(t, &organization.Organization{ID: 3}),
Team: unittest.AssertExistsAndLoadBean(t, &organization.Team{ID: 7}),
IsPull: util.OptionalBoolFalse,
},
issues_model.IssueStats{
YourRepositoriesCount: 2,
@@ -306,7 +315,7 @@ func TestGetUserIssueStats(t *testing.T) {
},
} {
t.Run(fmt.Sprintf("%#v", test.Opts), func(t *testing.T) {
stats, err := issues_model.GetUserIssueStats(test.Opts)
stats, err := issues_model.GetUserIssueStats(test.FilterMode, test.Opts)
if !assert.NoError(t, err) {
return
}
@@ -495,7 +504,7 @@ func TestCorrectIssueStats(t *testing.T) {
// Now we will call the GetIssueStats with these IDs and if working,
// get the correct stats back.
issueStats, err := issues_model.GetIssueStats(&issues_model.IssuesOptions{
RepoID: 1,
RepoIDs: []int64{1},
IssueIDs: ids,
})

View File

@@ -81,7 +81,7 @@ func doChangeIssueStatus(ctx context.Context, issue *Issue, doer *user_model.Use
}
// Update issue count of labels
if err := issue.getLabels(ctx); err != nil {
if err := issue.LoadLabels(ctx); err != nil {
return nil, err
}
for idx := range issue.Labels {

File diff suppressed because it is too large Load Diff

View File

@@ -302,7 +302,7 @@ func populateIssueIndexer(ctx context.Context) {
// UpdateRepoIndexer add/update all issues of the repositories
func UpdateRepoIndexer(ctx context.Context, repo *repo_model.Repository) {
is, err := issues_model.Issues(ctx, &issues_model.IssuesOptions{
RepoID: repo.ID,
RepoIDs: []int64{repo.ID},
IsClosed: util.OptionalBoolNone,
IsPull: util.OptionalBoolNone,
})

View File

@@ -470,7 +470,7 @@ func ListIssues(ctx *context.APIContext) {
if len(keyword) == 0 || len(issueIDs) > 0 || len(labelIDs) > 0 {
issuesOpt := &issues_model.IssuesOptions{
ListOptions: listOptions,
RepoID: ctx.Repo.Repository.ID,
RepoIDs: []int64{ctx.Repo.Repository.ID},
IsClosed: isClosed,
IssueIDs: issueIDs,
LabelIDs: labelIDs,

View File

@@ -207,7 +207,7 @@ func issues(ctx *context.Context, milestoneID, projectID int64, isPullOption uti
issueStats = &issues_model.IssueStats{}
} else {
issueStats, err = issues_model.GetIssueStats(&issues_model.IssuesOptions{
RepoID: repo.ID,
RepoIDs: []int64{repo.ID},
LabelIDs: labelIDs,
MilestoneIDs: []int64{milestoneID},
ProjectID: projectID,
@@ -258,7 +258,7 @@ func issues(ctx *context.Context, milestoneID, projectID int64, isPullOption uti
Page: pager.Paginater.Current(),
PageSize: setting.UI.IssuePagingNum,
},
RepoID: repo.ID,
RepoIDs: []int64{repo.ID},
AssigneeID: assigneeID,
PosterID: posterID,
MentionedID: mentionedID,
@@ -2652,7 +2652,7 @@ func ListIssues(ctx *context.Context) {
if len(keyword) == 0 || len(issueIDs) > 0 || len(labelIDs) > 0 {
issuesOpt := &issues_model.IssuesOptions{
ListOptions: listOptions,
RepoID: ctx.Repo.Repository.ID,
RepoIDs: []int64{ctx.Repo.Repository.ID},
IsClosed: isClosed,
IssueIDs: issueIDs,
LabelIDs: labelIDs,

View File

@@ -521,10 +521,7 @@ func buildIssueOverview(ctx *context.Context, unitType unit.Type) {
// Parse ctx.FormString("repos") and remember matched repo IDs for later.
// Gets set when clicking filters on the issues overview page.
repoIDs := getRepoIDs(ctx.FormString("repos"))
if len(repoIDs) > 0 {
opts.RepoCond = builder.In("issue.repo_id", repoIDs)
}
opts.RepoIDs = getRepoIDs(ctx.FormString("repos"))
// ------------------------------
// Get issues as defined by opts.
@@ -580,11 +577,10 @@ func buildIssueOverview(ctx *context.Context, unitType unit.Type) {
// -------------------------------
var issueStats *issues_model.IssueStats
if !forceEmpty {
statsOpts := issues_model.UserIssueStatsOptions{
UserID: ctx.Doer.ID,
FilterMode: filterMode,
IsPull: isPullList,
IsClosed: isShowClosed,
statsOpts := issues_model.IssuesOptions{
User: ctx.Doer,
IsPull: util.OptionalBoolOf(isPullList),
IsClosed: util.OptionalBoolOf(isShowClosed),
IssueIDs: issueIDsFromSearch,
IsArchived: util.OptionalBoolFalse,
LabelIDs: opts.LabelIDs,
@@ -593,7 +589,7 @@ func buildIssueOverview(ctx *context.Context, unitType unit.Type) {
RepoCond: opts.RepoCond,
}
issueStats, err = issues_model.GetUserIssueStats(statsOpts)
issueStats, err = issues_model.GetUserIssueStats(filterMode, statsOpts)
if err != nil {
ctx.ServerError("GetUserIssueStats Shown", err)
return
@@ -609,9 +605,9 @@ func buildIssueOverview(ctx *context.Context, unitType unit.Type) {
} else {
shownIssues = int(issueStats.ClosedCount)
}
if len(repoIDs) != 0 {
if len(opts.RepoIDs) != 0 {
shownIssues = 0
for _, repoID := range repoIDs {
for _, repoID := range opts.RepoIDs {
shownIssues += int(issueCountByRepo[repoID])
}
}
@@ -622,8 +618,8 @@ func buildIssueOverview(ctx *context.Context, unitType unit.Type) {
}
ctx.Data["TotalIssueCount"] = allIssueCount
if len(repoIDs) == 1 {
repo := showReposMap[repoIDs[0]]
if len(opts.RepoIDs) == 1 {
repo := showReposMap[opts.RepoIDs[0]]
if repo != nil {
ctx.Data["SingleRepoLink"] = repo.Link()
}
@@ -665,7 +661,7 @@ func buildIssueOverview(ctx *context.Context, unitType unit.Type) {
ctx.Data["IssueStats"] = issueStats
ctx.Data["ViewType"] = viewType
ctx.Data["SortType"] = sortType
ctx.Data["RepoIDs"] = repoIDs
ctx.Data["RepoIDs"] = opts.RepoIDs
ctx.Data["IsShowClosed"] = isShowClosed
ctx.Data["SelectLabels"] = selectedLabels
@@ -676,7 +672,7 @@ func buildIssueOverview(ctx *context.Context, unitType unit.Type) {
}
// Convert []int64 to string
reposParam, _ := json.Marshal(repoIDs)
reposParam, _ := json.Marshal(opts.RepoIDs)
ctx.Data["ReposParam"] = string(reposParam)

View File

@@ -104,7 +104,7 @@ func TestGiteaUploadRepo(t *testing.T) {
assert.Len(t, releases, 1)
issues, err := issues_model.Issues(db.DefaultContext, &issues_model.IssuesOptions{
RepoID: repo.ID,
RepoIDs: []int64{repo.ID},
IsPull: util.OptionalBoolFalse,
SortType: "oldest",
})