aboutsummaryrefslogtreecommitdiffstats
path: root/models/issues
diff options
context:
space:
mode:
Diffstat (limited to 'models/issues')
-rw-r--r--models/issues/comment.go20
-rw-r--r--models/issues/comment_code.go5
-rw-r--r--models/issues/comment_list.go35
-rw-r--r--models/issues/comment_test.go18
-rw-r--r--models/issues/issue.go3
-rw-r--r--models/issues/issue_label.go1
-rw-r--r--models/issues/issue_list.go45
-rw-r--r--models/issues/issue_list_test.go18
-rw-r--r--models/issues/issue_lock.go10
-rw-r--r--models/issues/issue_search.go48
-rw-r--r--models/issues/issue_stats.go5
-rw-r--r--models/issues/issue_test.go40
-rw-r--r--models/issues/issue_update.go152
-rw-r--r--models/issues/label.go3
-rw-r--r--models/issues/label_test.go28
-rw-r--r--models/issues/milestone_test.go6
-rw-r--r--models/issues/pull.go39
-rw-r--r--models/issues/pull_list.go3
-rw-r--r--models/issues/pull_list_test.go2
-rw-r--r--models/issues/pull_test.go59
-rw-r--r--models/issues/review.go11
-rw-r--r--models/issues/review_list.go2
-rw-r--r--models/issues/stopwatch.go167
-rw-r--r--models/issues/stopwatch_test.go61
-rw-r--r--models/issues/tracked_time.go5
25 files changed, 290 insertions, 496 deletions
diff --git a/models/issues/comment.go b/models/issues/comment.go
index ab9b2042f3..4fdb0c1808 100644
--- a/models/issues/comment.go
+++ b/models/issues/comment.go
@@ -9,6 +9,7 @@ import (
"context"
"fmt"
"html/template"
+ "slices"
"strconv"
"unicode/utf8"
@@ -196,12 +197,7 @@ func (t CommentType) HasMailReplySupport() bool {
}
func (t CommentType) CountedAsConversation() bool {
- for _, ct := range ConversationCountedCommentType() {
- if t == ct {
- return true
- }
- }
- return false
+ return slices.Contains(ConversationCountedCommentType(), t)
}
// ConversationCountedCommentType returns the comment types that are counted as a conversation
@@ -614,7 +610,7 @@ func UpdateCommentAttachments(ctx context.Context, c *Comment, uuids []string) e
if err != nil {
return fmt.Errorf("getAttachmentsByUUIDs [uuids: %v]: %w", uuids, err)
}
- for i := 0; i < len(attachments); i++ {
+ for i := range attachments {
attachments[i].IssueID = c.IssueID
attachments[i].CommentID = c.ID
if err := repo_model.UpdateAttachment(ctx, attachments[i]); err != nil {
@@ -719,7 +715,8 @@ func (c *Comment) LoadReactions(ctx context.Context, repo *repo_model.Repository
return nil
}
-func (c *Comment) loadReview(ctx context.Context) (err error) {
+// LoadReview loads the associated review
+func (c *Comment) LoadReview(ctx context.Context) (err error) {
if c.ReviewID == 0 {
return nil
}
@@ -736,11 +733,6 @@ func (c *Comment) loadReview(ctx context.Context) (err error) {
return nil
}
-// LoadReview loads the associated review
-func (c *Comment) LoadReview(ctx context.Context) error {
- return c.loadReview(ctx)
-}
-
// DiffSide returns "previous" if Comment.Line is a LOC of the previous changes and "proposed" if it is a LOC of the proposed changes.
func (c *Comment) DiffSide() string {
if c.Line < 0 {
@@ -860,7 +852,7 @@ func updateCommentInfos(ctx context.Context, opts *CreateCommentOptions, comment
}
if comment.ReviewID != 0 {
if comment.Review == nil {
- if err := comment.loadReview(ctx); err != nil {
+ if err := comment.LoadReview(ctx); err != nil {
return err
}
}
diff --git a/models/issues/comment_code.go b/models/issues/comment_code.go
index b562aab500..55e67a1243 100644
--- a/models/issues/comment_code.go
+++ b/models/issues/comment_code.go
@@ -5,6 +5,7 @@ package issues
import (
"context"
+ "strconv"
"code.gitea.io/gitea/models/db"
"code.gitea.io/gitea/models/renderhelper"
@@ -114,7 +115,9 @@ func findCodeComments(ctx context.Context, opts FindCommentsOptions, issue *Issu
}
var err error
- rctx := renderhelper.NewRenderContextRepoComment(ctx, issue.Repo)
+ rctx := renderhelper.NewRenderContextRepoComment(ctx, issue.Repo, renderhelper.RepoCommentOptions{
+ FootnoteContextID: strconv.FormatInt(comment.ID, 10),
+ })
if comment.RenderedContent, err = markdown.RenderString(rctx, comment.Content); err != nil {
return nil, err
}
diff --git a/models/issues/comment_list.go b/models/issues/comment_list.go
index c483ada75a..f6c485449f 100644
--- a/models/issues/comment_list.go
+++ b/models/issues/comment_list.go
@@ -57,10 +57,7 @@ func (comments CommentList) loadLabels(ctx context.Context) error {
commentLabels := make(map[int64]*Label, len(labelIDs))
left := len(labelIDs)
for left > 0 {
- limit := db.DefaultMaxInSize
- if left < limit {
- limit = left
- }
+ limit := min(left, db.DefaultMaxInSize)
rows, err := db.GetEngine(ctx).
In("id", labelIDs[:limit]).
Rows(new(Label))
@@ -107,10 +104,7 @@ func (comments CommentList) loadMilestones(ctx context.Context) error {
milestoneMaps := make(map[int64]*Milestone, len(milestoneIDs))
left := len(milestoneIDs)
for left > 0 {
- limit := db.DefaultMaxInSize
- if left < limit {
- limit = left
- }
+ limit := min(left, db.DefaultMaxInSize)
err := db.GetEngine(ctx).
In("id", milestoneIDs[:limit]).
Find(&milestoneMaps)
@@ -146,10 +140,7 @@ func (comments CommentList) loadOldMilestones(ctx context.Context) error {
milestoneMaps := make(map[int64]*Milestone, len(milestoneIDs))
left := len(milestoneIDs)
for left > 0 {
- limit := db.DefaultMaxInSize
- if left < limit {
- limit = left
- }
+ limit := min(left, db.DefaultMaxInSize)
err := db.GetEngine(ctx).
In("id", milestoneIDs[:limit]).
Find(&milestoneMaps)
@@ -184,10 +175,7 @@ func (comments CommentList) loadAssignees(ctx context.Context) error {
assignees := make(map[int64]*user_model.User, len(assigneeIDs))
left := len(assigneeIDs)
for left > 0 {
- limit := db.DefaultMaxInSize
- if left < limit {
- limit = left
- }
+ limit := min(left, db.DefaultMaxInSize)
rows, err := db.GetEngine(ctx).
In("id", assigneeIDs[:limit]).
Rows(new(user_model.User))
@@ -256,10 +244,7 @@ func (comments CommentList) LoadIssues(ctx context.Context) error {
issues := make(map[int64]*Issue, len(issueIDs))
left := len(issueIDs)
for left > 0 {
- limit := db.DefaultMaxInSize
- if left < limit {
- limit = left
- }
+ limit := min(left, db.DefaultMaxInSize)
rows, err := db.GetEngine(ctx).
In("id", issueIDs[:limit]).
Rows(new(Issue))
@@ -313,10 +298,7 @@ func (comments CommentList) loadDependentIssues(ctx context.Context) error {
issues := make(map[int64]*Issue, len(issueIDs))
left := len(issueIDs)
for left > 0 {
- limit := db.DefaultMaxInSize
- if left < limit {
- limit = left
- }
+ limit := min(left, db.DefaultMaxInSize)
rows, err := e.
In("id", issueIDs[:limit]).
Rows(new(Issue))
@@ -392,10 +374,7 @@ func (comments CommentList) LoadAttachments(ctx context.Context) (err error) {
commentsIDs := comments.getAttachmentCommentIDs()
left := len(commentsIDs)
for left > 0 {
- limit := db.DefaultMaxInSize
- if left < limit {
- limit = left
- }
+ limit := min(left, db.DefaultMaxInSize)
rows, err := db.GetEngine(ctx).
In("comment_id", commentsIDs[:limit]).
Rows(new(repo_model.Attachment))
diff --git a/models/issues/comment_test.go b/models/issues/comment_test.go
index ae0bc3ce17..c08e3b970d 100644
--- a/models/issues/comment_test.go
+++ b/models/issues/comment_test.go
@@ -34,10 +34,10 @@ func TestCreateComment(t *testing.T) {
assert.NoError(t, err)
then := time.Now().Unix()
- assert.EqualValues(t, issues_model.CommentTypeComment, comment.Type)
- assert.EqualValues(t, "Hello", comment.Content)
- assert.EqualValues(t, issue.ID, comment.IssueID)
- assert.EqualValues(t, doer.ID, comment.PosterID)
+ assert.Equal(t, issues_model.CommentTypeComment, comment.Type)
+ assert.Equal(t, "Hello", comment.Content)
+ assert.Equal(t, issue.ID, comment.IssueID)
+ assert.Equal(t, doer.ID, comment.PosterID)
unittest.AssertInt64InRange(t, now, then, int64(comment.CreatedUnix))
unittest.AssertExistsAndLoadBean(t, comment) // assert actually added to DB
@@ -58,9 +58,9 @@ func Test_UpdateCommentAttachment(t *testing.T) {
assert.NoError(t, err)
attachment2 := unittest.AssertExistsAndLoadBean(t, &repo_model.Attachment{ID: attachment.ID})
- assert.EqualValues(t, attachment.Name, attachment2.Name)
- assert.EqualValues(t, comment.ID, attachment2.CommentID)
- assert.EqualValues(t, comment.IssueID, attachment2.IssueID)
+ assert.Equal(t, attachment.Name, attachment2.Name)
+ assert.Equal(t, comment.ID, attachment2.CommentID)
+ assert.Equal(t, comment.IssueID, attachment2.IssueID)
}
func TestFetchCodeComments(t *testing.T) {
@@ -111,7 +111,7 @@ func TestMigrate_InsertIssueComments(t *testing.T) {
assert.NoError(t, err)
issueModified := unittest.AssertExistsAndLoadBean(t, &issues_model.Issue{ID: 1})
- assert.EqualValues(t, issue.NumComments+1, issueModified.NumComments)
+ assert.Equal(t, issue.NumComments+1, issueModified.NumComments)
unittest.CheckConsistencyFor(t, &issues_model.Issue{})
}
@@ -122,5 +122,5 @@ func Test_UpdateIssueNumComments(t *testing.T) {
assert.NoError(t, issues_model.UpdateIssueNumComments(db.DefaultContext, issue2.ID))
issue2 = unittest.AssertExistsAndLoadBean(t, &issues_model.Issue{ID: 2})
- assert.EqualValues(t, 1, issue2.NumComments)
+ assert.Equal(t, 1, issue2.NumComments)
}
diff --git a/models/issues/issue.go b/models/issues/issue.go
index 5204f27faf..a86d50ca9d 100644
--- a/models/issues/issue.go
+++ b/models/issues/issue.go
@@ -10,6 +10,7 @@ import (
"html/template"
"regexp"
"slices"
+ "strconv"
"code.gitea.io/gitea/models/db"
project_model "code.gitea.io/gitea/models/project"
@@ -815,7 +816,7 @@ func ChangeIssueTimeEstimate(ctx context.Context, issue *Issue, doer *user_model
Doer: doer,
Repo: issue.Repo,
Issue: issue,
- Content: fmt.Sprintf("%d", timeEstimate),
+ Content: strconv.FormatInt(timeEstimate, 10),
}
if _, err := CreateComment(ctx, opts); err != nil {
return fmt.Errorf("createComment: %w", err)
diff --git a/models/issues/issue_label.go b/models/issues/issue_label.go
index 10fc821454..f082079e07 100644
--- a/models/issues/issue_label.go
+++ b/models/issues/issue_label.go
@@ -206,6 +206,7 @@ func DeleteIssueLabel(ctx context.Context, issue *Issue, label *Label, doer *use
}
issue.Labels = nil
+ issue.isLabelsLoaded = false
return issue.LoadLabels(ctx)
}
diff --git a/models/issues/issue_list.go b/models/issues/issue_list.go
index 6c74b533b3..26b93189b8 100644
--- a/models/issues/issue_list.go
+++ b/models/issues/issue_list.go
@@ -42,10 +42,7 @@ func (issues IssueList) LoadRepositories(ctx context.Context) (repo_model.Reposi
repoMaps := make(map[int64]*repo_model.Repository, len(repoIDs))
left := len(repoIDs)
for left > 0 {
- limit := db.DefaultMaxInSize
- if left < limit {
- limit = left
- }
+ limit := min(left, db.DefaultMaxInSize)
err := db.GetEngine(ctx).
In("id", repoIDs[:limit]).
Find(&repoMaps)
@@ -116,10 +113,7 @@ func (issues IssueList) LoadLabels(ctx context.Context) error {
issueIDs := issues.getIssueIDs()
left := len(issueIDs)
for left > 0 {
- limit := db.DefaultMaxInSize
- if left < limit {
- limit = left
- }
+ limit := min(left, db.DefaultMaxInSize)
rows, err := db.GetEngine(ctx).Table("label").
Join("LEFT", "issue_label", "issue_label.label_id = label.id").
In("issue_label.issue_id", issueIDs[:limit]).
@@ -171,10 +165,7 @@ func (issues IssueList) LoadMilestones(ctx context.Context) error {
milestoneMaps := make(map[int64]*Milestone, len(milestoneIDs))
left := len(milestoneIDs)
for left > 0 {
- limit := db.DefaultMaxInSize
- if left < limit {
- limit = left
- }
+ limit := min(left, db.DefaultMaxInSize)
err := db.GetEngine(ctx).
In("id", milestoneIDs[:limit]).
Find(&milestoneMaps)
@@ -203,10 +194,7 @@ func (issues IssueList) LoadProjects(ctx context.Context) error {
}
for left > 0 {
- limit := db.DefaultMaxInSize
- if left < limit {
- limit = left
- }
+ limit := min(left, db.DefaultMaxInSize)
projects := make([]*projectWithIssueID, 0, limit)
err := db.GetEngine(ctx).
@@ -245,10 +233,7 @@ func (issues IssueList) LoadAssignees(ctx context.Context) error {
issueIDs := issues.getIssueIDs()
left := len(issueIDs)
for left > 0 {
- limit := db.DefaultMaxInSize
- if left < limit {
- limit = left
- }
+ limit := min(left, db.DefaultMaxInSize)
rows, err := db.GetEngine(ctx).Table("issue_assignees").
Join("INNER", "`user`", "`user`.id = `issue_assignees`.assignee_id").
In("`issue_assignees`.issue_id", issueIDs[:limit]).OrderBy(user_model.GetOrderByName()).
@@ -306,10 +291,7 @@ func (issues IssueList) LoadPullRequests(ctx context.Context) error {
pullRequestMaps := make(map[int64]*PullRequest, len(issuesIDs))
left := len(issuesIDs)
for left > 0 {
- limit := db.DefaultMaxInSize
- if left < limit {
- limit = left
- }
+ limit := min(left, db.DefaultMaxInSize)
rows, err := db.GetEngine(ctx).
In("issue_id", issuesIDs[:limit]).
Rows(new(PullRequest))
@@ -354,10 +336,7 @@ func (issues IssueList) LoadAttachments(ctx context.Context) (err error) {
issuesIDs := issues.getIssueIDs()
left := len(issuesIDs)
for left > 0 {
- limit := db.DefaultMaxInSize
- if left < limit {
- limit = left
- }
+ limit := min(left, db.DefaultMaxInSize)
rows, err := db.GetEngine(ctx).
In("issue_id", issuesIDs[:limit]).
Rows(new(repo_model.Attachment))
@@ -399,10 +378,7 @@ func (issues IssueList) loadComments(ctx context.Context, cond builder.Cond) (er
issuesIDs := issues.getIssueIDs()
left := len(issuesIDs)
for left > 0 {
- limit := db.DefaultMaxInSize
- if left < limit {
- limit = left
- }
+ limit := min(left, db.DefaultMaxInSize)
rows, err := db.GetEngine(ctx).Table("comment").
Join("INNER", "issue", "issue.id = comment.issue_id").
In("issue.id", issuesIDs[:limit]).
@@ -466,10 +442,7 @@ func (issues IssueList) loadTotalTrackedTimes(ctx context.Context) (err error) {
left := len(ids)
for left > 0 {
- limit := db.DefaultMaxInSize
- if left < limit {
- limit = left
- }
+ limit := min(left, db.DefaultMaxInSize)
// select issue_id, sum(time) from tracked_time where issue_id in (<issue ids in current page>) group by issue_id
rows, err := db.GetEngine(ctx).Table("tracked_time").
diff --git a/models/issues/issue_list_test.go b/models/issues/issue_list_test.go
index 9069e1012d..5b4d2ca5ab 100644
--- a/models/issues/issue_list_test.go
+++ b/models/issues/issue_list_test.go
@@ -27,7 +27,7 @@ func TestIssueList_LoadRepositories(t *testing.T) {
assert.NoError(t, err)
assert.Len(t, repos, 2)
for _, issue := range issueList {
- assert.EqualValues(t, issue.RepoID, issue.Repo.ID)
+ assert.Equal(t, issue.RepoID, issue.Repo.ID)
}
}
@@ -41,28 +41,28 @@ func TestIssueList_LoadAttributes(t *testing.T) {
assert.NoError(t, issueList.LoadAttributes(db.DefaultContext))
for _, issue := range issueList {
- assert.EqualValues(t, issue.RepoID, issue.Repo.ID)
+ assert.Equal(t, issue.RepoID, issue.Repo.ID)
for _, label := range issue.Labels {
- assert.EqualValues(t, issue.RepoID, label.RepoID)
+ assert.Equal(t, issue.RepoID, label.RepoID)
unittest.AssertExistsAndLoadBean(t, &issues_model.IssueLabel{IssueID: issue.ID, LabelID: label.ID})
}
if issue.PosterID > 0 {
- assert.EqualValues(t, issue.PosterID, issue.Poster.ID)
+ assert.Equal(t, issue.PosterID, issue.Poster.ID)
}
if issue.AssigneeID > 0 {
- assert.EqualValues(t, issue.AssigneeID, issue.Assignee.ID)
+ assert.Equal(t, issue.AssigneeID, issue.Assignee.ID)
}
if issue.MilestoneID > 0 {
- assert.EqualValues(t, issue.MilestoneID, issue.Milestone.ID)
+ assert.Equal(t, issue.MilestoneID, issue.Milestone.ID)
}
if issue.IsPull {
- assert.EqualValues(t, issue.ID, issue.PullRequest.IssueID)
+ assert.Equal(t, issue.ID, issue.PullRequest.IssueID)
}
for _, attachment := range issue.Attachments {
- assert.EqualValues(t, issue.ID, attachment.IssueID)
+ assert.Equal(t, issue.ID, attachment.IssueID)
}
for _, comment := range issue.Comments {
- assert.EqualValues(t, issue.ID, comment.IssueID)
+ assert.Equal(t, issue.ID, comment.IssueID)
}
if issue.ID == int64(1) {
assert.Equal(t, int64(400), issue.TotalTrackedTime)
diff --git a/models/issues/issue_lock.go b/models/issues/issue_lock.go
index b21629b529..fa0d128f74 100644
--- a/models/issues/issue_lock.go
+++ b/models/issues/issue_lock.go
@@ -12,8 +12,14 @@ import (
// IssueLockOptions defines options for locking and/or unlocking an issue/PR
type IssueLockOptions struct {
- Doer *user_model.User
- Issue *Issue
+ Doer *user_model.User
+ Issue *Issue
+
+ // Reason is the doer-provided comment message for the locked issue
+ // GitHub doesn't support changing the "reasons" by config file, so GitHub has pre-defined "reason" enum values.
+ // Gitea is not like GitHub, it allows site admin to define customized "reasons" in the config file.
+ // So the API caller might not know what kind of "reasons" are valid, and the customized reasons are not translatable.
+ // To make things clear and simple: doer have the chance to use any reason they like, we do not do validation.
Reason string
}
diff --git a/models/issues/issue_search.go b/models/issues/issue_search.go
index 694b918755..79bd6a19b0 100644
--- a/models/issues/issue_search.go
+++ b/models/issues/issue_search.go
@@ -21,14 +21,16 @@ import (
"xorm.io/xorm"
)
+const ScopeSortPrefix = "scope-"
+
// IssuesOptions represents options of an issue.
-type IssuesOptions struct { //nolint
+type IssuesOptions struct { //nolint:revive // export stutter
Paginator *db.ListOptions
RepoIDs []int64 // overwrites RepoCond if the length is not 0
AllPublic bool // include also all public repositories
RepoCond builder.Cond
- AssigneeID optional.Option[int64]
- PosterID optional.Option[int64]
+ AssigneeID string // "(none)" or "(any)" or a user ID
+ PosterID string // "(none)" or "(any)" or a user ID
MentionedID int64
ReviewRequestedID int64
ReviewedID int64
@@ -70,11 +72,24 @@ func (o *IssuesOptions) Copy(edit ...func(options *IssuesOptions)) *IssuesOption
// applySorts sort an issues-related session based on the provided
// sortType string
func applySorts(sess *xorm.Session, sortType string, priorityRepoID int64) {
+ // Since this sortType is dynamically created, it has to be treated specially.
+ if after, ok := strings.CutPrefix(sortType, ScopeSortPrefix); ok {
+ scope := after
+ sess.Join("LEFT", "issue_label", "issue.id = issue_label.issue_id")
+ // "exclusive_order=0" means "no order is set", so exclude it from the JOIN criteria and then "LEFT JOIN" result is also null
+ sess.Join("LEFT", "label", "label.id = issue_label.label_id AND label.exclusive_order <> 0 AND label.name LIKE ?", scope+"/%")
+ // Use COALESCE to make sure we sort NULL last regardless of backend DB (2147483647 == max int)
+ sess.OrderBy("COALESCE(label.exclusive_order, 2147483647) ASC").Desc("issue.id")
+ return
+ }
+
switch sortType {
case "oldest":
sess.Asc("issue.created_unix").Asc("issue.id")
case "recentupdate":
sess.Desc("issue.updated_unix").Desc("issue.created_unix").Desc("issue.id")
+ case "recentclose":
+ sess.Desc("issue.closed_unix").Desc("issue.created_unix").Desc("issue.id")
case "leastupdate":
sess.Asc("issue.updated_unix").Asc("issue.created_unix").Asc("issue.id")
case "mostcomment":
@@ -356,26 +371,25 @@ func issuePullAccessibleRepoCond(repoIDstr string, userID int64, owner *user_mod
return cond
}
-func applyAssigneeCondition(sess *xorm.Session, assigneeID optional.Option[int64]) {
+func applyAssigneeCondition(sess *xorm.Session, assigneeID string) {
// old logic: 0 is also treated as "not filtering assignee", because the "assignee" was read as FormInt64
- if !assigneeID.Has() || assigneeID.Value() == 0 {
- return
- }
- if assigneeID.Value() == db.NoConditionID {
+ if assigneeID == "(none)" {
sess.Where("issue.id NOT IN (SELECT issue_id FROM issue_assignees)")
- } else {
+ } else if assigneeID == "(any)" {
+ sess.Where("issue.id IN (SELECT issue_id FROM issue_assignees)")
+ } else if assigneeIDInt64, _ := strconv.ParseInt(assigneeID, 10, 64); assigneeIDInt64 > 0 {
sess.Join("INNER", "issue_assignees", "issue.id = issue_assignees.issue_id").
- And("issue_assignees.assignee_id = ?", assigneeID.Value())
+ And("issue_assignees.assignee_id = ?", assigneeIDInt64)
}
}
-func applyPosterCondition(sess *xorm.Session, posterID optional.Option[int64]) {
- if !posterID.Has() {
- return
- }
- // poster doesn't need to support db.NoConditionID(-1), so just use the value as-is
- if posterID.Has() {
- sess.And("issue.poster_id=?", posterID.Value())
+func applyPosterCondition(sess *xorm.Session, posterID string) {
+ // Actually every issue has a poster.
+ // The "(none)" is for internal usage only: when doer tries to search non-existing user as poster, use "(none)" to return empty result.
+ if posterID == "(none)" {
+ sess.And("issue.poster_id=0")
+ } else if posterIDInt64, _ := strconv.ParseInt(posterID, 10, 64); posterIDInt64 > 0 {
+ sess.And("issue.poster_id=?", posterIDInt64)
}
}
diff --git a/models/issues/issue_stats.go b/models/issues/issue_stats.go
index 50409fbbd8..adedaa3d3a 100644
--- a/models/issues/issue_stats.go
+++ b/models/issues/issue_stats.go
@@ -94,10 +94,7 @@ func GetIssueStats(ctx context.Context, opts *IssuesOptions) (*IssueStats, error
// ids in a temporary table and join from them.
accum := &IssueStats{}
for i := 0; i < len(opts.IssueIDs); {
- chunk := i + MaxQueryParameters
- if chunk > len(opts.IssueIDs) {
- chunk = len(opts.IssueIDs)
- }
+ chunk := min(i+MaxQueryParameters, len(opts.IssueIDs))
stats, err := getIssueStatsChunk(ctx, opts, opts.IssueIDs[i:chunk])
if err != nil {
return nil, err
diff --git a/models/issues/issue_test.go b/models/issues/issue_test.go
index 3f76a81bb6..1c5db55bbc 100644
--- a/models/issues/issue_test.go
+++ b/models/issues/issue_test.go
@@ -5,6 +5,7 @@ package issues_test
import (
"fmt"
+ "slices"
"sort"
"sync"
"testing"
@@ -15,7 +16,6 @@ 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/optional"
"code.gitea.io/gitea/modules/setting"
"github.com/stretchr/testify/assert"
@@ -142,8 +142,8 @@ func TestUpdateIssueCols(t *testing.T) {
then := time.Now().Unix()
updatedIssue := unittest.AssertExistsAndLoadBean(t, &issues_model.Issue{ID: issue.ID})
- assert.EqualValues(t, newTitle, updatedIssue.Title)
- assert.EqualValues(t, prevContent, updatedIssue.Content)
+ assert.Equal(t, newTitle, updatedIssue.Title)
+ assert.Equal(t, prevContent, updatedIssue.Content)
unittest.AssertInt64InRange(t, now, then, int64(updatedIssue.UpdatedUnix))
}
@@ -155,7 +155,7 @@ func TestIssues(t *testing.T) {
}{
{
issues_model.IssuesOptions{
- AssigneeID: optional.Some(int64(1)),
+ AssigneeID: "1",
SortType: "oldest",
},
[]int64{1, 6},
@@ -202,7 +202,7 @@ func TestIssues(t *testing.T) {
assert.NoError(t, err)
if assert.Len(t, issues, len(test.ExpectedIssueIDs)) {
for i, issue := range issues {
- assert.EqualValues(t, test.ExpectedIssueIDs[i], issue.ID)
+ assert.Equal(t, test.ExpectedIssueIDs[i], issue.ID)
}
}
}
@@ -235,10 +235,10 @@ func testInsertIssue(t *testing.T, title, content string, expectIndex int64) *is
has, err := db.GetEngine(db.DefaultContext).ID(issue.ID).Get(&newIssue)
assert.NoError(t, err)
assert.True(t, has)
- assert.EqualValues(t, issue.Title, newIssue.Title)
- assert.EqualValues(t, issue.Content, newIssue.Content)
+ assert.Equal(t, issue.Title, newIssue.Title)
+ assert.Equal(t, issue.Content, newIssue.Content)
if expectIndex > 0 {
- assert.EqualValues(t, expectIndex, newIssue.Index)
+ assert.Equal(t, expectIndex, newIssue.Index)
}
})
return &newIssue
@@ -271,8 +271,8 @@ func TestIssue_ResolveMentions(t *testing.T) {
for i, user := range resolved {
ids[i] = user.ID
}
- sort.Slice(ids, func(i, j int) bool { return ids[i] < ids[j] })
- assert.EqualValues(t, expected, ids)
+ slices.Sort(ids)
+ assert.Equal(t, expected, ids)
}
// Public repo, existing user
@@ -293,7 +293,7 @@ func TestResourceIndex(t *testing.T) {
assert.NoError(t, unittest.PrepareTestDatabase())
var wg sync.WaitGroup
- for i := 0; i < 100; i++ {
+ for i := range 100 {
wg.Add(1)
go func(i int) {
testInsertIssue(t, fmt.Sprintf("issue %d", i+1), "my issue", 0)
@@ -315,7 +315,7 @@ func TestCorrectIssueStats(t *testing.T) {
issueAmount := issues_model.MaxQueryParameters + 10
var wg sync.WaitGroup
- for i := 0; i < issueAmount; i++ {
+ for i := range issueAmount {
wg.Add(1)
go func(i int) {
testInsertIssue(t, fmt.Sprintf("Issue %d", i+1), "Bugs are nasty", 0)
@@ -393,28 +393,28 @@ func TestIssueLoadAttributes(t *testing.T) {
for _, issue := range issueList {
assert.NoError(t, issue.LoadAttributes(db.DefaultContext))
- assert.EqualValues(t, issue.RepoID, issue.Repo.ID)
+ assert.Equal(t, issue.RepoID, issue.Repo.ID)
for _, label := range issue.Labels {
- assert.EqualValues(t, issue.RepoID, label.RepoID)
+ assert.Equal(t, issue.RepoID, label.RepoID)
unittest.AssertExistsAndLoadBean(t, &issues_model.IssueLabel{IssueID: issue.ID, LabelID: label.ID})
}
if issue.PosterID > 0 {
- assert.EqualValues(t, issue.PosterID, issue.Poster.ID)
+ assert.Equal(t, issue.PosterID, issue.Poster.ID)
}
if issue.AssigneeID > 0 {
- assert.EqualValues(t, issue.AssigneeID, issue.Assignee.ID)
+ assert.Equal(t, issue.AssigneeID, issue.Assignee.ID)
}
if issue.MilestoneID > 0 {
- assert.EqualValues(t, issue.MilestoneID, issue.Milestone.ID)
+ assert.Equal(t, issue.MilestoneID, issue.Milestone.ID)
}
if issue.IsPull {
- assert.EqualValues(t, issue.ID, issue.PullRequest.IssueID)
+ assert.Equal(t, issue.ID, issue.PullRequest.IssueID)
}
for _, attachment := range issue.Attachments {
- assert.EqualValues(t, issue.ID, attachment.IssueID)
+ assert.Equal(t, issue.ID, attachment.IssueID)
}
for _, comment := range issue.Comments {
- assert.EqualValues(t, issue.ID, comment.IssueID)
+ assert.Equal(t, issue.ID, comment.IssueID)
}
if issue.ID == int64(1) {
assert.Equal(t, int64(400), issue.TotalTrackedTime)
diff --git a/models/issues/issue_update.go b/models/issues/issue_update.go
index 7b3fe04aa5..9b99787e3b 100644
--- a/models/issues/issue_update.go
+++ b/models/issues/issue_update.go
@@ -5,16 +5,14 @@ package issues
import (
"context"
+ "errors"
"fmt"
"strings"
"code.gitea.io/gitea/models/db"
"code.gitea.io/gitea/models/organization"
- "code.gitea.io/gitea/models/perm"
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"
- system_model "code.gitea.io/gitea/models/system"
"code.gitea.io/gitea/models/unit"
user_model "code.gitea.io/gitea/models/user"
"code.gitea.io/gitea/modules/git"
@@ -306,7 +304,7 @@ func UpdateIssueAttachments(ctx context.Context, issueID int64, uuids []string)
if err != nil {
return fmt.Errorf("getAttachmentsByUUIDs [uuids: %v]: %w", uuids, err)
}
- for i := 0; i < len(attachments); i++ {
+ for i := range attachments {
attachments[i].IssueID = issueID
if err := repo_model.UpdateAttachment(ctx, attachments[i]); err != nil {
return fmt.Errorf("update attachment [id: %d]: %w", attachments[i].ID, err)
@@ -386,10 +384,10 @@ func NewIssueWithIndex(ctx context.Context, doer *user_model.User, opts NewIssue
}
if opts.Issue.Index <= 0 {
- return fmt.Errorf("no issue index provided")
+ return errors.New("no issue index provided")
}
if opts.Issue.ID > 0 {
- return fmt.Errorf("issue exist")
+ return errors.New("issue exist")
}
if _, err := e.Insert(opts.Issue); err != nil {
@@ -611,7 +609,7 @@ func ResolveIssueMentionsByVisibility(ctx context.Context, issue *Issue, doer *u
unittype = unit.TypePullRequests
}
for _, team := range teams {
- if team.AccessMode >= perm.AccessModeAdmin {
+ if team.HasAdminAccess() {
checked = append(checked, team.ID)
resolved[issue.Repo.Owner.LowerName+"/"+team.LowerName] = true
continue
@@ -715,137 +713,13 @@ func UpdateReactionsMigrationsByType(ctx context.Context, gitServiceType api.Git
return err
}
-// DeleteIssuesByRepoID deletes issues by repositories id
-func DeleteIssuesByRepoID(ctx context.Context, repoID int64) (attachmentPaths []string, err error) {
- // MariaDB has a performance bug: https://jira.mariadb.org/browse/MDEV-16289
- // so here it uses "DELETE ... WHERE IN" with pre-queried IDs.
- sess := db.GetEngine(ctx)
-
- for {
- issueIDs := make([]int64, 0, db.DefaultMaxInSize)
-
- err := sess.Table(&Issue{}).Where("repo_id = ?", repoID).OrderBy("id").Limit(db.DefaultMaxInSize).Cols("id").Find(&issueIDs)
- if err != nil {
- return nil, err
- }
-
- if len(issueIDs) == 0 {
- break
- }
-
- // Delete content histories
- _, err = sess.In("issue_id", issueIDs).Delete(&ContentHistory{})
- if err != nil {
- return nil, err
- }
-
- // Delete comments and attachments
- _, err = sess.In("issue_id", issueIDs).Delete(&Comment{})
- if err != nil {
- return nil, err
- }
-
- // Dependencies for issues in this repository
- _, err = sess.In("issue_id", issueIDs).Delete(&IssueDependency{})
- if err != nil {
- return nil, err
- }
-
- // Delete dependencies for issues in other repositories
- _, err = sess.In("dependency_id", issueIDs).Delete(&IssueDependency{})
- if err != nil {
- return nil, err
- }
-
- _, err = sess.In("issue_id", issueIDs).Delete(&IssueUser{})
- if err != nil {
- return nil, err
- }
-
- _, err = sess.In("issue_id", issueIDs).Delete(&Reaction{})
- if err != nil {
- return nil, err
- }
-
- _, err = sess.In("issue_id", issueIDs).Delete(&IssueWatch{})
- if err != nil {
- return nil, err
- }
-
- _, err = sess.In("issue_id", issueIDs).Delete(&Stopwatch{})
- if err != nil {
- return nil, err
- }
-
- _, err = sess.In("issue_id", issueIDs).Delete(&TrackedTime{})
- if err != nil {
- return nil, err
- }
-
- _, err = sess.In("issue_id", issueIDs).Delete(&project_model.ProjectIssue{})
- if err != nil {
- return nil, err
- }
-
- _, err = sess.In("dependent_issue_id", issueIDs).Delete(&Comment{})
- if err != nil {
- return nil, err
- }
-
- var attachments []*repo_model.Attachment
- err = sess.In("issue_id", issueIDs).Find(&attachments)
- if err != nil {
- return nil, err
- }
-
- for j := range attachments {
- attachmentPaths = append(attachmentPaths, attachments[j].RelativePath())
- }
-
- _, err = sess.In("issue_id", issueIDs).Delete(&repo_model.Attachment{})
- if err != nil {
- return nil, err
- }
-
- _, err = sess.In("id", issueIDs).Delete(&Issue{})
- if err != nil {
- return nil, err
- }
- }
-
- return attachmentPaths, err
-}
-
-// DeleteOrphanedIssues delete issues without a repo
-func DeleteOrphanedIssues(ctx context.Context) error {
- var attachmentPaths []string
- err := db.WithTx(ctx, func(ctx context.Context) error {
- var ids []int64
-
- if err := db.GetEngine(ctx).Table("issue").Distinct("issue.repo_id").
- Join("LEFT", "repository", "issue.repo_id=repository.id").
- Where(builder.IsNull{"repository.id"}).GroupBy("issue.repo_id").
- Find(&ids); err != nil {
- return err
- }
-
- for i := range ids {
- paths, err := DeleteIssuesByRepoID(ctx, ids[i])
- if err != nil {
- return err
- }
- attachmentPaths = append(attachmentPaths, paths...)
- }
-
- return nil
- })
- if err != nil {
- return err
- }
-
- // Remove issue attachment files.
- for i := range attachmentPaths {
- system_model.RemoveAllWithNotice(ctx, "Delete issue attachment", attachmentPaths[i])
+func GetOrphanedIssueRepoIDs(ctx context.Context) ([]int64, error) {
+ var repoIDs []int64
+ if err := db.GetEngine(ctx).Table("issue").Distinct("issue.repo_id").
+ Join("LEFT", "repository", "issue.repo_id=repository.id").
+ Where(builder.IsNull{"repository.id"}).
+ Find(&repoIDs); err != nil {
+ return nil, err
}
- return nil
+ return repoIDs, nil
}
diff --git a/models/issues/label.go b/models/issues/label.go
index 8a5d9321cc..cfbe100926 100644
--- a/models/issues/label.go
+++ b/models/issues/label.go
@@ -87,6 +87,7 @@ type Label struct {
OrgID int64 `xorm:"INDEX"`
Name string
Exclusive bool
+ ExclusiveOrder int `xorm:"DEFAULT 0"` // 0 means no exclusive order
Description string
Color string `xorm:"VARCHAR(7)"`
NumIssues int
@@ -236,7 +237,7 @@ func UpdateLabel(ctx context.Context, l *Label) error {
}
l.Color = color
- return updateLabelCols(ctx, l, "name", "description", "color", "exclusive", "archived_unix")
+ return updateLabelCols(ctx, l, "name", "description", "color", "exclusive", "exclusive_order", "archived_unix")
}
// DeleteLabel delete a label
diff --git a/models/issues/label_test.go b/models/issues/label_test.go
index 185fa11bbc..226036d543 100644
--- a/models/issues/label_test.go
+++ b/models/issues/label_test.go
@@ -20,7 +20,7 @@ func TestLabel_CalOpenIssues(t *testing.T) {
assert.NoError(t, unittest.PrepareTestDatabase())
label := unittest.AssertExistsAndLoadBean(t, &issues_model.Label{ID: 1})
label.CalOpenIssues()
- assert.EqualValues(t, 2, label.NumOpenIssues)
+ assert.Equal(t, 2, label.NumOpenIssues)
}
func TestLabel_LoadSelectedLabelsAfterClick(t *testing.T) {
@@ -154,7 +154,7 @@ func TestGetLabelsByRepoID(t *testing.T) {
assert.NoError(t, err)
assert.Len(t, labels, len(expectedIssueIDs))
for i, label := range labels {
- assert.EqualValues(t, expectedIssueIDs[i], label.ID)
+ assert.Equal(t, expectedIssueIDs[i], label.ID)
}
}
testSuccess(1, "leastissues", []int64{2, 1})
@@ -221,7 +221,7 @@ func TestGetLabelsByOrgID(t *testing.T) {
assert.NoError(t, err)
assert.Len(t, labels, len(expectedIssueIDs))
for i, label := range labels {
- assert.EqualValues(t, expectedIssueIDs[i], label.ID)
+ assert.Equal(t, expectedIssueIDs[i], label.ID)
}
}
testSuccess(3, "leastissues", []int64{3, 4})
@@ -267,10 +267,10 @@ func TestUpdateLabel(t *testing.T) {
label.Name = update.Name
assert.NoError(t, issues_model.UpdateLabel(db.DefaultContext, update))
newLabel := unittest.AssertExistsAndLoadBean(t, &issues_model.Label{ID: 1})
- assert.EqualValues(t, label.ID, newLabel.ID)
- assert.EqualValues(t, label.Color, newLabel.Color)
- assert.EqualValues(t, label.Name, newLabel.Name)
- assert.EqualValues(t, label.Description, newLabel.Description)
+ assert.Equal(t, label.ID, newLabel.ID)
+ assert.Equal(t, label.Color, newLabel.Color)
+ assert.Equal(t, label.Name, newLabel.Name)
+ assert.Equal(t, label.Description, newLabel.Description)
assert.EqualValues(t, 0, newLabel.ArchivedUnix)
unittest.CheckConsistencyFor(t, &issues_model.Label{}, &repo_model.Repository{})
}
@@ -313,7 +313,7 @@ func TestNewIssueLabel(t *testing.T) {
Content: "1",
})
label = unittest.AssertExistsAndLoadBean(t, &issues_model.Label{ID: 2})
- assert.EqualValues(t, prevNumIssues+1, label.NumIssues)
+ assert.Equal(t, prevNumIssues+1, label.NumIssues)
// re-add existing IssueLabel
assert.NoError(t, issues_model.NewIssueLabel(db.DefaultContext, issue, label, doer))
@@ -366,11 +366,11 @@ func TestNewIssueLabels(t *testing.T) {
})
unittest.AssertExistsAndLoadBean(t, &issues_model.IssueLabel{IssueID: issue.ID, LabelID: label1.ID})
label1 = unittest.AssertExistsAndLoadBean(t, &issues_model.Label{ID: 1})
- assert.EqualValues(t, 3, label1.NumIssues)
- assert.EqualValues(t, 1, label1.NumClosedIssues)
+ assert.Equal(t, 3, label1.NumIssues)
+ assert.Equal(t, 1, label1.NumClosedIssues)
label2 = unittest.AssertExistsAndLoadBean(t, &issues_model.Label{ID: 2})
- assert.EqualValues(t, 1, label2.NumIssues)
- assert.EqualValues(t, 1, label2.NumClosedIssues)
+ assert.Equal(t, 1, label2.NumIssues)
+ assert.Equal(t, 1, label2.NumClosedIssues)
// corner case: test empty slice
assert.NoError(t, issues_model.NewIssueLabels(db.DefaultContext, issue, []*issues_model.Label{}, doer))
@@ -408,8 +408,8 @@ func TestDeleteIssueLabel(t *testing.T) {
LabelID: labelID,
}, `content=''`)
label = unittest.AssertExistsAndLoadBean(t, &issues_model.Label{ID: labelID})
- assert.EqualValues(t, expectedNumIssues, label.NumIssues)
- assert.EqualValues(t, expectedNumClosedIssues, label.NumClosedIssues)
+ assert.Equal(t, expectedNumIssues, label.NumIssues)
+ assert.Equal(t, expectedNumClosedIssues, label.NumClosedIssues)
}
testSuccess(1, 1, 2)
testSuccess(2, 5, 2)
diff --git a/models/issues/milestone_test.go b/models/issues/milestone_test.go
index 28cd0c028b..f73355c27d 100644
--- a/models/issues/milestone_test.go
+++ b/models/issues/milestone_test.go
@@ -69,7 +69,7 @@ func TestGetMilestonesByRepoID(t *testing.T) {
assert.Len(t, milestones, n)
for _, milestone := range milestones {
- assert.EqualValues(t, repoID, milestone.RepoID)
+ assert.Equal(t, repoID, milestone.RepoID)
}
}
test(1, api.StateOpen)
@@ -327,7 +327,7 @@ func TestUpdateMilestone(t *testing.T) {
milestone.Content = "newMilestoneContent"
assert.NoError(t, issues_model.UpdateMilestone(db.DefaultContext, milestone, milestone.IsClosed))
milestone = unittest.AssertExistsAndLoadBean(t, &issues_model.Milestone{ID: 1})
- assert.EqualValues(t, "newMilestoneName", milestone.Name)
+ assert.Equal(t, "newMilestoneName", milestone.Name)
unittest.CheckConsistencyFor(t, &issues_model.Milestone{})
}
@@ -364,7 +364,7 @@ func TestMigrate_InsertMilestones(t *testing.T) {
assert.NoError(t, err)
unittest.AssertExistsAndLoadBean(t, ms)
repoModified := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: repo.ID})
- assert.EqualValues(t, repo.NumMilestones+1, repoModified.NumMilestones)
+ assert.Equal(t, repo.NumMilestones+1, repoModified.NumMilestones)
unittest.CheckConsistencyFor(t, &issues_model.Milestone{})
}
diff --git a/models/issues/pull.go b/models/issues/pull.go
index e3af00224d..0ff32e2473 100644
--- a/models/issues/pull.go
+++ b/models/issues/pull.go
@@ -6,10 +6,10 @@ package issues
import (
"context"
+ "errors"
"fmt"
"io"
"regexp"
- "strconv"
"strings"
"code.gitea.io/gitea/models/db"
@@ -103,27 +103,6 @@ const (
PullRequestStatusAncestor
)
-func (status PullRequestStatus) String() string {
- switch status {
- case PullRequestStatusConflict:
- return "CONFLICT"
- case PullRequestStatusChecking:
- return "CHECKING"
- case PullRequestStatusMergeable:
- return "MERGEABLE"
- case PullRequestStatusManuallyMerged:
- return "MANUALLY_MERGED"
- case PullRequestStatusError:
- return "ERROR"
- case PullRequestStatusEmpty:
- return "EMPTY"
- case PullRequestStatusAncestor:
- return "ANCESTOR"
- default:
- return strconv.Itoa(int(status))
- }
-}
-
// PullRequestFlow the flow of pull request
type PullRequestFlow int
@@ -670,12 +649,6 @@ func GetAllUnmergedAgitPullRequestByPoster(ctx context.Context, uid int64) ([]*P
return pulls, err
}
-// Update updates all fields of pull request.
-func (pr *PullRequest) Update(ctx context.Context) error {
- _, err := db.GetEngine(ctx).ID(pr.ID).AllCols().Update(pr)
- return err
-}
-
// UpdateCols updates specific fields of pull request.
func (pr *PullRequest) UpdateCols(ctx context.Context, cols ...string) error {
_, err := db.GetEngine(ctx).ID(pr.ID).Cols(cols...).Update(pr)
@@ -732,7 +705,7 @@ func (pr *PullRequest) GetWorkInProgressPrefix(ctx context.Context) string {
// UpdateCommitDivergence update Divergence of a pull request
func (pr *PullRequest) UpdateCommitDivergence(ctx context.Context, ahead, behind int) error {
if pr.ID == 0 {
- return fmt.Errorf("pull ID is 0")
+ return errors.New("pull ID is 0")
}
pr.CommitsAhead = ahead
pr.CommitsBehind = behind
@@ -925,7 +898,7 @@ func ParseCodeOwnersLine(ctx context.Context, tokens []string) (*CodeOwnerRule,
if strings.Contains(user, "/") {
s := strings.Split(user, "/")
if len(s) != 2 {
- warnings = append(warnings, fmt.Sprintf("incorrect codeowner group: %s", user))
+ warnings = append(warnings, "incorrect codeowner group: "+user)
continue
}
orgName := s[0]
@@ -933,12 +906,12 @@ func ParseCodeOwnersLine(ctx context.Context, tokens []string) (*CodeOwnerRule,
org, err := org_model.GetOrgByName(ctx, orgName)
if err != nil {
- warnings = append(warnings, fmt.Sprintf("incorrect codeowner organization: %s", user))
+ warnings = append(warnings, "incorrect codeowner organization: "+user)
continue
}
teams, err := org.LoadTeams(ctx)
if err != nil {
- warnings = append(warnings, fmt.Sprintf("incorrect codeowner team: %s", user))
+ warnings = append(warnings, "incorrect codeowner team: "+user)
continue
}
@@ -950,7 +923,7 @@ func ParseCodeOwnersLine(ctx context.Context, tokens []string) (*CodeOwnerRule,
} else {
u, err := user_model.GetUserByName(ctx, user)
if err != nil {
- warnings = append(warnings, fmt.Sprintf("incorrect codeowner user: %s", user))
+ warnings = append(warnings, "incorrect codeowner user: "+user)
continue
}
rule.Users = append(rule.Users, u)
diff --git a/models/issues/pull_list.go b/models/issues/pull_list.go
index b685175f8e..84f9f6166d 100644
--- a/models/issues/pull_list.go
+++ b/models/issues/pull_list.go
@@ -152,7 +152,8 @@ func PullRequests(ctx context.Context, baseRepoID int64, opts *PullRequestsOptio
applySorts(findSession, opts.SortType, 0)
findSession = db.SetSessionPagination(findSession, opts)
prs := make([]*PullRequest, 0, opts.PageSize)
- return prs, maxResults, findSession.Find(&prs)
+ found := findSession.Find(&prs)
+ return prs, maxResults, found
}
// PullRequestList defines a list of pull requests
diff --git a/models/issues/pull_list_test.go b/models/issues/pull_list_test.go
index f5553e7885..eb2de006d6 100644
--- a/models/issues/pull_list_test.go
+++ b/models/issues/pull_list_test.go
@@ -40,7 +40,7 @@ func TestPullRequestList_LoadReviewCommentsCounts(t *testing.T) {
assert.NoError(t, err)
assert.Len(t, reviewComments, 2)
for _, pr := range prs {
- assert.EqualValues(t, 1, reviewComments[pr.IssueID])
+ assert.Equal(t, 1, reviewComments[pr.IssueID])
}
}
diff --git a/models/issues/pull_test.go b/models/issues/pull_test.go
index 090659864a..39efaa5792 100644
--- a/models/issues/pull_test.go
+++ b/models/issues/pull_test.go
@@ -14,6 +14,7 @@ import (
"code.gitea.io/gitea/modules/setting"
"github.com/stretchr/testify/assert"
+ "github.com/stretchr/testify/require"
)
func TestPullRequest_LoadAttributes(t *testing.T) {
@@ -76,6 +77,47 @@ func TestPullRequestsNewest(t *testing.T) {
}
}
+func TestPullRequests_Closed_RecentSortType(t *testing.T) {
+ // Issue ID | Closed At. | Updated At
+ // 2 | 1707270001 | 1707270001
+ // 3 | 1707271000 | 1707279999
+ // 11 | 1707279999 | 1707275555
+ tests := []struct {
+ sortType string
+ expectedIssueIDOrder []int64
+ }{
+ {"recentupdate", []int64{3, 11, 2}},
+ {"recentclose", []int64{11, 3, 2}},
+ }
+
+ assert.NoError(t, unittest.PrepareTestDatabase())
+ _, err := db.Exec(db.DefaultContext, "UPDATE issue SET closed_unix = 1707270001, updated_unix = 1707270001, is_closed = true WHERE id = 2")
+ require.NoError(t, err)
+ _, err = db.Exec(db.DefaultContext, "UPDATE issue SET closed_unix = 1707271000, updated_unix = 1707279999, is_closed = true WHERE id = 3")
+ require.NoError(t, err)
+ _, err = db.Exec(db.DefaultContext, "UPDATE issue SET closed_unix = 1707279999, updated_unix = 1707275555, is_closed = true WHERE id = 11")
+ require.NoError(t, err)
+
+ for _, test := range tests {
+ t.Run(test.sortType, func(t *testing.T) {
+ prs, _, err := issues_model.PullRequests(db.DefaultContext, 1, &issues_model.PullRequestsOptions{
+ ListOptions: db.ListOptions{
+ Page: 1,
+ },
+ State: "closed",
+ SortType: test.sortType,
+ })
+ require.NoError(t, err)
+
+ if assert.Len(t, prs, len(test.expectedIssueIDOrder)) {
+ for i := range test.expectedIssueIDOrder {
+ assert.Equal(t, test.expectedIssueIDOrder[i], prs[i].IssueID)
+ }
+ }
+ })
+ }
+}
+
func TestLoadRequestedReviewers(t *testing.T) {
assert.NoError(t, unittest.PrepareTestDatabase())
@@ -206,19 +248,6 @@ func TestGetPullRequestByIssueID(t *testing.T) {
assert.True(t, issues_model.IsErrPullRequestNotExist(err))
}
-func TestPullRequest_Update(t *testing.T) {
- assert.NoError(t, unittest.PrepareTestDatabase())
- pr := unittest.AssertExistsAndLoadBean(t, &issues_model.PullRequest{ID: 1})
- pr.BaseBranch = "baseBranch"
- pr.HeadBranch = "headBranch"
- pr.Update(db.DefaultContext)
-
- pr = unittest.AssertExistsAndLoadBean(t, &issues_model.PullRequest{ID: pr.ID})
- assert.Equal(t, "baseBranch", pr.BaseBranch)
- assert.Equal(t, "headBranch", pr.HeadBranch)
- unittest.CheckConsistencyFor(t, pr)
-}
-
func TestPullRequest_UpdateCols(t *testing.T) {
assert.NoError(t, unittest.PrepareTestDatabase())
pr := &issues_model.PullRequest{
@@ -285,7 +314,7 @@ func TestDeleteOrphanedObjects(t *testing.T) {
countAfter, err := db.GetEngine(db.DefaultContext).Count(&issues_model.PullRequest{})
assert.NoError(t, err)
- assert.EqualValues(t, countBefore, countAfter)
+ assert.Equal(t, countBefore, countAfter)
}
func TestParseCodeOwnersLine(t *testing.T) {
@@ -318,7 +347,7 @@ func TestGetApprovers(t *testing.T) {
setting.Repository.PullRequest.DefaultMergeMessageOfficialApproversOnly = false
approvers := pr.GetApprovers(db.DefaultContext)
expected := "Reviewed-by: User Five <user5@example.com>\nReviewed-by: Org Six <org6@example.com>\n"
- assert.EqualValues(t, expected, approvers)
+ assert.Equal(t, expected, approvers)
}
func TestGetPullRequestByMergedCommit(t *testing.T) {
diff --git a/models/issues/review.go b/models/issues/review.go
index 1c5c2ee30a..71fdb7456f 100644
--- a/models/issues/review.go
+++ b/models/issues/review.go
@@ -5,6 +5,7 @@ package issues
import (
"context"
+ "errors"
"fmt"
"slices"
"strings"
@@ -374,7 +375,7 @@ func CreateReview(ctx context.Context, opts CreateReviewOptions) (*Review, error
review.Type = ReviewTypeRequest
review.ReviewerTeamID = opts.ReviewerTeam.ID
} else {
- return nil, fmt.Errorf("provide either reviewer or reviewer team")
+ return nil, errors.New("provide either reviewer or reviewer team")
}
if _, err := sess.Insert(review); err != nil {
@@ -663,7 +664,7 @@ func AddReviewRequest(ctx context.Context, issue *Issue, reviewer, doer *user_mo
}
if review != nil {
- // skip it when reviewer hase been request to review
+ // skip it when reviewer has been request to review
if review.Type == ReviewTypeRequest {
return nil, committer.Commit() // still commit the transaction, or committer.Close() will rollback it, even if it's a reused transaction.
}
@@ -933,7 +934,7 @@ func MarkConversation(ctx context.Context, comment *Comment, doer *user_model.Us
// the PR writer , official reviewer and poster can do it
func CanMarkConversation(ctx context.Context, issue *Issue, doer *user_model.User) (permResult bool, err error) {
if doer == nil || issue == nil {
- return false, fmt.Errorf("issue or doer is nil")
+ return false, errors.New("issue or doer is nil")
}
if err = issue.LoadRepo(ctx); err != nil {
@@ -972,11 +973,11 @@ func DeleteReview(ctx context.Context, r *Review) error {
defer committer.Close()
if r.ID == 0 {
- return fmt.Errorf("review is not allowed to be 0")
+ return errors.New("review is not allowed to be 0")
}
if r.Type == ReviewTypeRequest {
- return fmt.Errorf("review request can not be deleted using this method")
+ return errors.New("review request can not be deleted using this method")
}
opts := FindCommentsOptions{
diff --git a/models/issues/review_list.go b/models/issues/review_list.go
index 928f24fb2d..bbb8c489fa 100644
--- a/models/issues/review_list.go
+++ b/models/issues/review_list.go
@@ -22,7 +22,7 @@ type ReviewList []*Review
// LoadReviewers loads reviewers
func (reviews ReviewList) LoadReviewers(ctx context.Context) error {
reviewerIDs := make([]int64, len(reviews))
- for i := 0; i < len(reviews); i++ {
+ for i := range reviews {
reviewerIDs[i] = reviews[i].ReviewerID
}
reviewers, err := user_model.GetPossibleUserByIDs(ctx, reviewerIDs)
diff --git a/models/issues/stopwatch.go b/models/issues/stopwatch.go
index 7c05a3a883..761b8f91a0 100644
--- a/models/issues/stopwatch.go
+++ b/models/issues/stopwatch.go
@@ -5,7 +5,6 @@ package issues
import (
"context"
- "fmt"
"time"
"code.gitea.io/gitea/models/db"
@@ -15,20 +14,6 @@ import (
"code.gitea.io/gitea/modules/util"
)
-// ErrIssueStopwatchNotExist represents an error that stopwatch is not exist
-type ErrIssueStopwatchNotExist struct {
- UserID int64
- IssueID int64
-}
-
-func (err ErrIssueStopwatchNotExist) Error() string {
- return fmt.Sprintf("issue stopwatch doesn't exist[uid: %d, issue_id: %d", err.UserID, err.IssueID)
-}
-
-func (err ErrIssueStopwatchNotExist) Unwrap() error {
- return util.ErrNotExist
-}
-
// Stopwatch represents a stopwatch for time tracking.
type Stopwatch struct {
ID int64 `xorm:"pk autoincr"`
@@ -55,13 +40,11 @@ func getStopwatch(ctx context.Context, userID, issueID int64) (sw *Stopwatch, ex
return sw, exists, err
}
-// UserIDCount is a simple coalition of UserID and Count
type UserStopwatch struct {
UserID int64
StopWatches []*Stopwatch
}
-// GetUIDsAndNotificationCounts between the two provided times
func GetUIDsAndStopwatch(ctx context.Context) ([]*UserStopwatch, error) {
sws := []*Stopwatch{}
if err := db.GetEngine(ctx).Where("issue_id != 0").Find(&sws); err != nil {
@@ -87,7 +70,7 @@ func GetUIDsAndStopwatch(ctx context.Context) ([]*UserStopwatch, error) {
return res, nil
}
-// GetUserStopwatches return list of all stopwatches of a user
+// GetUserStopwatches return list of the user's all stopwatches
func GetUserStopwatches(ctx context.Context, userID int64, listOptions db.ListOptions) ([]*Stopwatch, error) {
sws := make([]*Stopwatch, 0, 8)
sess := db.GetEngine(ctx).Where("stopwatch.user_id = ?", userID)
@@ -102,7 +85,7 @@ func GetUserStopwatches(ctx context.Context, userID int64, listOptions db.ListOp
return sws, nil
}
-// CountUserStopwatches return count of all stopwatches of a user
+// CountUserStopwatches return count of the user's all stopwatches
func CountUserStopwatches(ctx context.Context, userID int64) (int64, error) {
return db.GetEngine(ctx).Where("user_id = ?", userID).Count(&Stopwatch{})
}
@@ -136,43 +119,21 @@ func HasUserStopwatch(ctx context.Context, userID int64) (exists bool, sw *Stopw
return exists, sw, issue, err
}
-// FinishIssueStopwatchIfPossible if stopwatch exist then finish it otherwise ignore
-func FinishIssueStopwatchIfPossible(ctx context.Context, user *user_model.User, issue *Issue) error {
- _, exists, err := getStopwatch(ctx, user.ID, issue.ID)
- if err != nil {
- return err
- }
- if !exists {
- return nil
- }
- return FinishIssueStopwatch(ctx, user, issue)
-}
-
-// CreateOrStopIssueStopwatch create an issue stopwatch if it's not exist, otherwise finish it
-func CreateOrStopIssueStopwatch(ctx context.Context, user *user_model.User, issue *Issue) error {
- _, exists, err := getStopwatch(ctx, user.ID, issue.ID)
- if err != nil {
- return err
- }
- if exists {
- return FinishIssueStopwatch(ctx, user, issue)
- }
- return CreateIssueStopwatch(ctx, user, issue)
-}
-
-// FinishIssueStopwatch if stopwatch exist then finish it otherwise return an error
-func FinishIssueStopwatch(ctx context.Context, user *user_model.User, issue *Issue) error {
+// FinishIssueStopwatch if stopwatch exists, then finish it.
+func FinishIssueStopwatch(ctx context.Context, user *user_model.User, issue *Issue) (ok bool, err error) {
sw, exists, err := getStopwatch(ctx, user.ID, issue.ID)
if err != nil {
- return err
+ return false, err
+ } else if !exists {
+ return false, nil
}
- if !exists {
- return ErrIssueStopwatchNotExist{
- UserID: user.ID,
- IssueID: issue.ID,
- }
+ if err = finishIssueStopwatch(ctx, user, issue, sw); err != nil {
+ return false, err
}
+ return true, nil
+}
+func finishIssueStopwatch(ctx context.Context, user *user_model.User, issue *Issue, sw *Stopwatch) error {
// Create tracked time out of the time difference between start date and actual date
timediff := time.Now().Unix() - int64(sw.CreatedUnix)
@@ -184,14 +145,12 @@ func FinishIssueStopwatch(ctx context.Context, user *user_model.User, issue *Iss
Time: timediff,
}
- if err := db.Insert(ctx, tt); err != nil {
+ if err := issue.LoadRepo(ctx); err != nil {
return err
}
-
- if err := issue.LoadRepo(ctx); err != nil {
+ if err := db.Insert(ctx, tt); err != nil {
return err
}
-
if _, err := CreateComment(ctx, &CreateCommentOptions{
Doer: user,
Issue: issue,
@@ -202,83 +161,65 @@ func FinishIssueStopwatch(ctx context.Context, user *user_model.User, issue *Iss
}); err != nil {
return err
}
- _, err = db.DeleteByBean(ctx, sw)
+ _, err := db.DeleteByBean(ctx, sw)
return err
}
-// CreateIssueStopwatch creates a stopwatch if not exist, otherwise return an error
-func CreateIssueStopwatch(ctx context.Context, user *user_model.User, issue *Issue) error {
- if err := issue.LoadRepo(ctx); err != nil {
- return err
- }
-
- // if another stopwatch is running: stop it
- exists, _, otherIssue, err := HasUserStopwatch(ctx, user.ID)
- if err != nil {
- return err
- }
- if exists {
- if err := FinishIssueStopwatch(ctx, user, otherIssue); err != nil {
- return err
+// CreateIssueStopwatch creates a stopwatch if the issue doesn't have the user's stopwatch.
+// It also stops any other stopwatch that might be running for the user.
+func CreateIssueStopwatch(ctx context.Context, user *user_model.User, issue *Issue) (ok bool, err error) {
+ { // if another issue's stopwatch is running: stop it; if this issue has a stopwatch: return an error.
+ exists, otherStopWatch, otherIssue, err := HasUserStopwatch(ctx, user.ID)
+ if err != nil {
+ return false, err
+ }
+ if exists {
+ if otherStopWatch.IssueID == issue.ID {
+ // don't allow starting stopwatch for the same issue
+ return false, nil
+ }
+ // stop the other issue's stopwatch
+ if err = finishIssueStopwatch(ctx, user, otherIssue, otherStopWatch); err != nil {
+ return false, err
+ }
}
}
- // Create stopwatch
- sw := &Stopwatch{
- UserID: user.ID,
- IssueID: issue.ID,
+ if err = issue.LoadRepo(ctx); err != nil {
+ return false, err
}
-
- if err := db.Insert(ctx, sw); err != nil {
- return err
+ if err = db.Insert(ctx, &Stopwatch{UserID: user.ID, IssueID: issue.ID}); err != nil {
+ return false, err
}
-
- if err := issue.LoadRepo(ctx); err != nil {
- return err
- }
-
- if _, err := CreateComment(ctx, &CreateCommentOptions{
+ if _, err = CreateComment(ctx, &CreateCommentOptions{
Doer: user,
Issue: issue,
Repo: issue.Repo,
Type: CommentTypeStartTracking,
}); err != nil {
- return err
+ return false, err
}
-
- return nil
+ return true, nil
}
// CancelStopwatch removes the given stopwatch and logs it into issue's timeline.
-func CancelStopwatch(ctx context.Context, user *user_model.User, issue *Issue) error {
- ctx, committer, err := db.TxContext(ctx)
- if err != nil {
- return err
- }
- defer committer.Close()
- if err := cancelStopwatch(ctx, user, issue); err != nil {
- return err
- }
- return committer.Commit()
-}
-
-func cancelStopwatch(ctx context.Context, user *user_model.User, issue *Issue) error {
- e := db.GetEngine(ctx)
- sw, exists, err := getStopwatch(ctx, user.ID, issue.ID)
- if err != nil {
- return err
- }
-
- if exists {
- if _, err := e.Delete(sw); err != nil {
+func CancelStopwatch(ctx context.Context, user *user_model.User, issue *Issue) (ok bool, err error) {
+ err = db.WithTx(ctx, func(ctx context.Context) error {
+ e := db.GetEngine(ctx)
+ sw, exists, err := getStopwatch(ctx, user.ID, issue.ID)
+ if err != nil {
return err
+ } else if !exists {
+ return nil
}
- if err := issue.LoadRepo(ctx); err != nil {
+ if err = issue.LoadRepo(ctx); err != nil {
return err
}
-
- if _, err := CreateComment(ctx, &CreateCommentOptions{
+ if _, err = e.Delete(sw); err != nil {
+ return err
+ }
+ if _, err = CreateComment(ctx, &CreateCommentOptions{
Doer: user,
Issue: issue,
Repo: issue.Repo,
@@ -286,6 +227,8 @@ func cancelStopwatch(ctx context.Context, user *user_model.User, issue *Issue) e
}); err != nil {
return err
}
- }
- return nil
+ ok = true
+ return nil
+ })
+ return ok, err
}
diff --git a/models/issues/stopwatch_test.go b/models/issues/stopwatch_test.go
index a1bf9dc931..6333c10234 100644
--- a/models/issues/stopwatch_test.go
+++ b/models/issues/stopwatch_test.go
@@ -10,7 +10,6 @@ import (
issues_model "code.gitea.io/gitea/models/issues"
"code.gitea.io/gitea/models/unittest"
user_model "code.gitea.io/gitea/models/user"
- "code.gitea.io/gitea/modules/timeutil"
"github.com/stretchr/testify/assert"
)
@@ -18,26 +17,22 @@ import (
func TestCancelStopwatch(t *testing.T) {
assert.NoError(t, unittest.PrepareTestDatabase())
- user1, err := user_model.GetUserByID(db.DefaultContext, 1)
- assert.NoError(t, err)
-
- issue1, err := issues_model.GetIssueByID(db.DefaultContext, 1)
- assert.NoError(t, err)
- issue2, err := issues_model.GetIssueByID(db.DefaultContext, 2)
- assert.NoError(t, err)
+ user1 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 1})
+ issue1 := unittest.AssertExistsAndLoadBean(t, &issues_model.Issue{ID: 1})
- err = issues_model.CancelStopwatch(db.DefaultContext, user1, issue1)
+ ok, err := issues_model.CancelStopwatch(db.DefaultContext, user1, issue1)
assert.NoError(t, err)
+ assert.True(t, ok)
unittest.AssertNotExistsBean(t, &issues_model.Stopwatch{UserID: user1.ID, IssueID: issue1.ID})
+ unittest.AssertExistsAndLoadBean(t, &issues_model.Comment{Type: issues_model.CommentTypeCancelTracking, PosterID: user1.ID, IssueID: issue1.ID})
- _ = unittest.AssertExistsAndLoadBean(t, &issues_model.Comment{Type: issues_model.CommentTypeCancelTracking, PosterID: user1.ID, IssueID: issue1.ID})
-
- assert.NoError(t, issues_model.CancelStopwatch(db.DefaultContext, user1, issue2))
+ ok, err = issues_model.CancelStopwatch(db.DefaultContext, user1, issue1)
+ assert.NoError(t, err)
+ assert.False(t, ok)
}
func TestStopwatchExists(t *testing.T) {
assert.NoError(t, unittest.PrepareTestDatabase())
-
assert.True(t, issues_model.StopwatchExists(db.DefaultContext, 1, 1))
assert.False(t, issues_model.StopwatchExists(db.DefaultContext, 1, 2))
}
@@ -58,21 +53,35 @@ func TestHasUserStopwatch(t *testing.T) {
func TestCreateOrStopIssueStopwatch(t *testing.T) {
assert.NoError(t, unittest.PrepareTestDatabase())
- user2, err := user_model.GetUserByID(db.DefaultContext, 2)
+ user4 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 4})
+ issue1 := unittest.AssertExistsAndLoadBean(t, &issues_model.Issue{ID: 1})
+ issue3 := unittest.AssertExistsAndLoadBean(t, &issues_model.Issue{ID: 3})
+
+ // create a new stopwatch
+ ok, err := issues_model.CreateIssueStopwatch(db.DefaultContext, user4, issue1)
assert.NoError(t, err)
- org3, err := user_model.GetUserByID(db.DefaultContext, 3)
+ assert.True(t, ok)
+ unittest.AssertExistsAndLoadBean(t, &issues_model.Stopwatch{UserID: user4.ID, IssueID: issue1.ID})
+ // should not create a second stopwatch for the same issue
+ ok, err = issues_model.CreateIssueStopwatch(db.DefaultContext, user4, issue1)
assert.NoError(t, err)
-
- issue1, err := issues_model.GetIssueByID(db.DefaultContext, 1)
+ assert.False(t, ok)
+ // on a different issue, it will finish the existing stopwatch and create a new one
+ ok, err = issues_model.CreateIssueStopwatch(db.DefaultContext, user4, issue3)
assert.NoError(t, err)
- issue2, err := issues_model.GetIssueByID(db.DefaultContext, 2)
+ assert.True(t, ok)
+ unittest.AssertNotExistsBean(t, &issues_model.Stopwatch{UserID: user4.ID, IssueID: issue1.ID})
+ unittest.AssertExistsAndLoadBean(t, &issues_model.Stopwatch{UserID: user4.ID, IssueID: issue3.ID})
+
+ // user2 already has a stopwatch in test fixture
+ user2 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2})
+ issue2 := unittest.AssertExistsAndLoadBean(t, &issues_model.Issue{ID: 2})
+ ok, err = issues_model.FinishIssueStopwatch(db.DefaultContext, user2, issue2)
assert.NoError(t, err)
-
- assert.NoError(t, issues_model.CreateOrStopIssueStopwatch(db.DefaultContext, org3, issue1))
- sw := unittest.AssertExistsAndLoadBean(t, &issues_model.Stopwatch{UserID: 3, IssueID: 1})
- assert.LessOrEqual(t, sw.CreatedUnix, timeutil.TimeStampNow())
-
- assert.NoError(t, issues_model.CreateOrStopIssueStopwatch(db.DefaultContext, user2, issue2))
- unittest.AssertNotExistsBean(t, &issues_model.Stopwatch{UserID: 2, IssueID: 2})
- unittest.AssertExistsAndLoadBean(t, &issues_model.TrackedTime{UserID: 2, IssueID: 2})
+ assert.True(t, ok)
+ unittest.AssertNotExistsBean(t, &issues_model.Stopwatch{UserID: user2.ID, IssueID: issue2.ID})
+ unittest.AssertExistsAndLoadBean(t, &issues_model.TrackedTime{UserID: user2.ID, IssueID: issue2.ID})
+ ok, err = issues_model.FinishIssueStopwatch(db.DefaultContext, user2, issue2)
+ assert.NoError(t, err)
+ assert.False(t, ok)
}
diff --git a/models/issues/tracked_time.go b/models/issues/tracked_time.go
index ea404d36cd..2afbe272ed 100644
--- a/models/issues/tracked_time.go
+++ b/models/issues/tracked_time.go
@@ -350,10 +350,7 @@ func GetIssueTotalTrackedTime(ctx context.Context, opts *IssuesOptions, isClosed
// we get the statistics in smaller chunks and get accumulates
var accum int64
for i := 0; i < len(opts.IssueIDs); {
- chunk := i + MaxQueryParameters
- if chunk > len(opts.IssueIDs) {
- chunk = len(opts.IssueIDs)
- }
+ chunk := min(i+MaxQueryParameters, len(opts.IssueIDs))
time, err := getIssueTotalTrackedTimeChunk(ctx, opts, isClosed, opts.IssueIDs[i:chunk])
if err != nil {
return 0, err