diff options
Diffstat (limited to 'models')
-rw-r--r-- | models/action.go | 28 | ||||
-rw-r--r-- | models/action_test.go | 43 | ||||
-rw-r--r-- | models/branch.go | 80 | ||||
-rw-r--r-- | models/consistency.go | 214 | ||||
-rw-r--r-- | models/consistency_test.go | 149 | ||||
-rw-r--r-- | models/db/consistency.go | 27 | ||||
-rw-r--r-- | models/db/index.go | 23 | ||||
-rw-r--r-- | models/db/list_options.go | 5 | ||||
-rw-r--r-- | models/error.go | 376 | ||||
-rw-r--r-- | models/git/branches_test.go | 6 | ||||
-rw-r--r-- | models/git/main_test.go | 1 | ||||
-rw-r--r-- | models/issue_label_test.go | 394 | ||||
-rw-r--r-- | models/issue_stopwatch_test.go | 78 | ||||
-rw-r--r-- | models/issue_user_test.go | 61 | ||||
-rw-r--r-- | models/issues/assignees.go (renamed from models/issue_assignees.go) | 2 | ||||
-rw-r--r-- | models/issues/assignees_test.go (renamed from models/issue_assignees_test.go) | 27 | ||||
-rw-r--r-- | models/issues/comment.go (renamed from models/issue_comment.go) | 106 | ||||
-rw-r--r-- | models/issues/comment_list.go (renamed from models/issue_comment_list.go) | 29 | ||||
-rw-r--r-- | models/issues/comment_test.go (renamed from models/issue_comment_test.go) | 19 | ||||
-rw-r--r-- | models/issues/content_history.go | 6 | ||||
-rw-r--r-- | models/issues/content_history_test.go | 43 | ||||
-rw-r--r-- | models/issues/dependency.go (renamed from models/issue_dependency.go) | 81 | ||||
-rw-r--r-- | models/issues/dependency_test.go (renamed from models/issue_dependency_test.go) | 27 | ||||
-rw-r--r-- | models/issues/issue.go (renamed from models/issue.go) | 397 | ||||
-rw-r--r-- | models/issues/issue_index.go | 32 | ||||
-rw-r--r-- | models/issues/issue_list.go (renamed from models/issue_list.go) | 28 | ||||
-rw-r--r-- | models/issues/issue_list_test.go (renamed from models/issue_list_test.go) | 19 | ||||
-rw-r--r-- | models/issues/issue_lock.go (renamed from models/issue_lock.go) | 2 | ||||
-rw-r--r-- | models/issues/issue_project.go (renamed from models/issue_project.go) | 2 | ||||
-rw-r--r-- | models/issues/issue_test.go (renamed from models/issue_test.go) | 206 | ||||
-rw-r--r-- | models/issues/issue_user.go (renamed from models/issue_user.go) | 5 | ||||
-rw-r--r-- | models/issues/issue_user_test.go | 62 | ||||
-rw-r--r-- | models/issues/issue_watch.go (renamed from models/issue_watch.go) | 5 | ||||
-rw-r--r-- | models/issues/issue_watch_test.go (renamed from models/issue_watch_test.go) | 25 | ||||
-rw-r--r-- | models/issues/issue_xref.go (renamed from models/issue_xref.go) | 26 | ||||
-rw-r--r-- | models/issues/issue_xref_test.go (renamed from models/issue_xref_test.go) | 69 | ||||
-rw-r--r-- | models/issues/label.go (renamed from models/issue_label.go) | 156 | ||||
-rw-r--r-- | models/issues/label_test.go | 395 | ||||
-rw-r--r-- | models/issues/main_test.go | 24 | ||||
-rw-r--r-- | models/issues/milestone.go | 33 | ||||
-rw-r--r-- | models/issues/milestone_test.go | 151 | ||||
-rw-r--r-- | models/issues/pull.go (renamed from models/pull.go) | 155 | ||||
-rw-r--r-- | models/issues/pull_list.go (renamed from models/pull_list.go) | 2 | ||||
-rw-r--r-- | models/issues/pull_test.go (renamed from models/pull_test.go) | 94 | ||||
-rw-r--r-- | models/issues/reaction_test.go | 31 | ||||
-rw-r--r-- | models/issues/review.go (renamed from models/review.go) | 53 | ||||
-rw-r--r-- | models/issues/review_test.go (renamed from models/review_test.go) | 111 | ||||
-rw-r--r-- | models/issues/stopwatch.go (renamed from models/issue_stopwatch.go) | 4 | ||||
-rw-r--r-- | models/issues/stopwatch_test.go | 79 | ||||
-rw-r--r-- | models/issues/tracked_time.go (renamed from models/issue_tracked_time.go) | 4 | ||||
-rw-r--r-- | models/issues/tracked_time_test.go (renamed from models/issue_tracked_time_test.go) | 35 | ||||
-rw-r--r-- | models/main_test.go | 5 | ||||
-rw-r--r-- | models/migrate.go | 49 | ||||
-rw-r--r-- | models/migrate_test.go | 28 | ||||
-rw-r--r-- | models/migrations/v111.go | 2 | ||||
-rw-r--r-- | models/notification.go | 58 | ||||
-rw-r--r-- | models/notification_test.go | 5 | ||||
-rw-r--r-- | models/org_team.go | 5 | ||||
-rw-r--r-- | models/repo.go | 20 | ||||
-rw-r--r-- | models/repo/repo.go | 47 | ||||
-rw-r--r-- | models/repo_activity.go | 21 | ||||
-rw-r--r-- | models/repo_collaboration.go | 5 | ||||
-rw-r--r-- | models/repo_transfer.go | 3 | ||||
-rw-r--r-- | models/statistic.go | 4 | ||||
-rw-r--r-- | models/user.go | 18 |
65 files changed, 2057 insertions, 2243 deletions
diff --git a/models/action.go b/models/action.go index 882bc59d8f..951328070d 100644 --- a/models/action.go +++ b/models/action.go @@ -15,6 +15,7 @@ import ( "time" "code.gitea.io/gitea/models/db" + issues_model "code.gitea.io/gitea/models/issues" "code.gitea.io/gitea/models/organization" access_model "code.gitea.io/gitea/models/perm/access" repo_model "code.gitea.io/gitea/models/repo" @@ -76,7 +77,7 @@ type Action struct { RepoID int64 `xorm:"INDEX"` Repo *repo_model.Repository `xorm:"-"` CommentID int64 `xorm:"INDEX"` - Comment *Comment `xorm:"-"` + Comment *issues_model.Comment `xorm:"-"` IsDeleted bool `xorm:"INDEX NOT NULL DEFAULT false"` RefName string IsPrivate bool `xorm:"INDEX NOT NULL DEFAULT false"` @@ -223,7 +224,7 @@ func (a *Action) getCommentLink(ctx context.Context) string { return "#" } if a.Comment == nil && a.CommentID != 0 { - a.Comment, _ = GetCommentByID(ctx, a.CommentID) + a.Comment, _ = issues_model.GetCommentByID(ctx, a.CommentID) } if a.Comment != nil { return a.Comment.HTMLURL() @@ -238,7 +239,7 @@ func (a *Action) getCommentLink(ctx context.Context) string { return "#" } - issue, err := getIssueByID(ctx, issueID) + issue, err := issues_model.GetIssueByID(ctx, issueID) if err != nil { return "#" } @@ -295,7 +296,7 @@ func (a *Action) GetIssueInfos() []string { // with the action. func (a *Action) GetIssueTitle() string { index, _ := strconv.ParseInt(a.GetIssueInfos()[0], 10, 64) - issue, err := GetIssueByIndex(a.RepoID, index) + issue, err := issues_model.GetIssueByIndex(a.RepoID, index) if err != nil { log.Error("GetIssueByIndex: %v", err) return "500 when get issue" @@ -307,7 +308,7 @@ func (a *Action) GetIssueTitle() string { // this action. func (a *Action) GetIssueContent() string { index, _ := strconv.ParseInt(a.GetIssueInfos()[0], 10, 64) - issue, err := GetIssueByIndex(a.RepoID, index) + issue, err := issues_model.GetIssueByIndex(a.RepoID, index) if err != nil { log.Error("GetIssueByIndex: %v", err) return "500 when get issue" @@ -572,3 +573,20 @@ func NotifyWatchersActions(acts []*Action) error { } return committer.Commit() } + +// DeleteIssueActions delete all actions related with issueID +func DeleteIssueActions(ctx context.Context, repoID, issueID int64) error { + // delete actions assigned to this issue + subQuery := builder.Select("`id`"). + From("`comment`"). + Where(builder.Eq{"`issue_id`": issueID}) + if _, err := db.GetEngine(ctx).In("comment_id", subQuery).Delete(&Action{}); err != nil { + return err + } + + _, err := db.GetEngine(ctx).Table("action").Where("repo_id = ?", repoID). + In("op_type", ActionCreateIssue, ActionCreatePullRequest). + Where("content LIKE ?", strconv.FormatInt(issueID, 10)+"|%"). + Delete(&Action{}) + return err +} diff --git a/models/action_test.go b/models/action_test.go index fb8a6c2686..2d46bd3e80 100644 --- a/models/action_test.go +++ b/models/action_test.go @@ -228,3 +228,46 @@ func TestGetFeedsCorrupted(t *testing.T) { assert.NoError(t, err) assert.Len(t, actions, 0) } + +func TestConsistencyUpdateAction(t *testing.T) { + if !setting.Database.UseSQLite3 { + t.Skip("Test is only for SQLite database.") + } + assert.NoError(t, unittest.PrepareTestDatabase()) + id := 8 + unittest.AssertExistsAndLoadBean(t, &Action{ + ID: int64(id), + }) + _, err := db.GetEngine(db.DefaultContext).Exec(`UPDATE action SET created_unix = "" WHERE id = ?`, id) + assert.NoError(t, err) + actions := make([]*Action, 0, 1) + // + // XORM returns an error when created_unix is a string + // + err = db.GetEngine(db.DefaultContext).Where("id = ?", id).Find(&actions) + if assert.Error(t, err) { + assert.Contains(t, err.Error(), "type string to a int64: invalid syntax") + } + // + // Get rid of incorrectly set created_unix + // + count, err := CountActionCreatedUnixString() + assert.NoError(t, err) + assert.EqualValues(t, 1, count) + count, err = FixActionCreatedUnixString() + assert.NoError(t, err) + assert.EqualValues(t, 1, count) + + count, err = CountActionCreatedUnixString() + assert.NoError(t, err) + assert.EqualValues(t, 0, count) + count, err = FixActionCreatedUnixString() + assert.NoError(t, err) + assert.EqualValues(t, 0, count) + + // + // XORM must be happy now + // + assert.NoError(t, db.GetEngine(db.DefaultContext).Where("id = ?", id).Find(&actions)) + unittest.CheckConsistencyFor(t, &Action{}) +} diff --git a/models/branch.go b/models/branch.go deleted file mode 100644 index 3d6e7d82e2..0000000000 --- a/models/branch.go +++ /dev/null @@ -1,80 +0,0 @@ -// Copyright 2022 The Gitea Authors. All rights reserved. -// Use of this source code is governed by a MIT-style -// license that can be found in the LICENSE file. - -package models - -import ( - "context" - - "code.gitea.io/gitea/models/db" - git_model "code.gitea.io/gitea/models/git" - "code.gitea.io/gitea/modules/log" -) - -// HasEnoughApprovals returns true if pr has enough granted approvals. -func HasEnoughApprovals(ctx context.Context, protectBranch *git_model.ProtectedBranch, pr *PullRequest) bool { - if protectBranch.RequiredApprovals == 0 { - return true - } - return GetGrantedApprovalsCount(ctx, protectBranch, pr) >= protectBranch.RequiredApprovals -} - -// GetGrantedApprovalsCount returns the number of granted approvals for pr. A granted approval must be authored by a user in an approval whitelist. -func GetGrantedApprovalsCount(ctx context.Context, protectBranch *git_model.ProtectedBranch, pr *PullRequest) int64 { - sess := db.GetEngine(ctx).Where("issue_id = ?", pr.IssueID). - And("type = ?", ReviewTypeApprove). - And("official = ?", true). - And("dismissed = ?", false) - if protectBranch.DismissStaleApprovals { - sess = sess.And("stale = ?", false) - } - approvals, err := sess.Count(new(Review)) - if err != nil { - log.Error("GetGrantedApprovalsCount: %v", err) - return 0 - } - - return approvals -} - -// MergeBlockedByRejectedReview returns true if merge is blocked by rejected reviews -func MergeBlockedByRejectedReview(ctx context.Context, protectBranch *git_model.ProtectedBranch, pr *PullRequest) bool { - if !protectBranch.BlockOnRejectedReviews { - return false - } - rejectExist, err := db.GetEngine(ctx).Where("issue_id = ?", pr.IssueID). - And("type = ?", ReviewTypeReject). - And("official = ?", true). - And("dismissed = ?", false). - Exist(new(Review)) - if err != nil { - log.Error("MergeBlockedByRejectedReview: %v", err) - return true - } - - return rejectExist -} - -// MergeBlockedByOfficialReviewRequests block merge because of some review request to official reviewer -// of from official review -func MergeBlockedByOfficialReviewRequests(ctx context.Context, protectBranch *git_model.ProtectedBranch, pr *PullRequest) bool { - if !protectBranch.BlockOnOfficialReviewRequests { - return false - } - has, err := db.GetEngine(ctx).Where("issue_id = ?", pr.IssueID). - And("type = ?", ReviewTypeRequest). - And("official = ?", true). - Exist(new(Review)) - if err != nil { - log.Error("MergeBlockedByOfficialReviewRequests: %v", err) - return true - } - - return has -} - -// MergeBlockedByOutdatedBranch returns true if merge is blocked by an outdated head branch -func MergeBlockedByOutdatedBranch(protectBranch *git_model.ProtectedBranch, pr *PullRequest) bool { - return protectBranch.BlockOnOutdatedBranch && pr.CommitsBehind > 0 -} diff --git a/models/consistency.go b/models/consistency.go index e817b69176..18ed9195fc 100644 --- a/models/consistency.go +++ b/models/consistency.go @@ -5,7 +5,6 @@ package models import ( - admin_model "code.gitea.io/gitea/models/admin" "code.gitea.io/gitea/models/db" repo_model "code.gitea.io/gitea/models/repo" user_model "code.gitea.io/gitea/models/user" @@ -14,151 +13,6 @@ import ( "xorm.io/builder" ) -// CountOrphanedLabels return count of labels witch are broken and not accessible via ui anymore -func CountOrphanedLabels() (int64, error) { - noref, err := db.GetEngine(db.DefaultContext).Table("label").Where("repo_id=? AND org_id=?", 0, 0).Count("label.id") - if err != nil { - return 0, err - } - - norepo, err := db.GetEngine(db.DefaultContext).Table("label"). - Where(builder.And( - builder.Gt{"repo_id": 0}, - builder.NotIn("repo_id", builder.Select("id").From("repository")), - )). - Count() - if err != nil { - return 0, err - } - - noorg, err := db.GetEngine(db.DefaultContext).Table("label"). - Where(builder.And( - builder.Gt{"org_id": 0}, - builder.NotIn("org_id", builder.Select("id").From("user")), - )). - Count() - if err != nil { - return 0, err - } - - return noref + norepo + noorg, nil -} - -// DeleteOrphanedLabels delete labels witch are broken and not accessible via ui anymore -func DeleteOrphanedLabels() error { - // delete labels with no reference - if _, err := db.GetEngine(db.DefaultContext).Table("label").Where("repo_id=? AND org_id=?", 0, 0).Delete(new(Label)); err != nil { - return err - } - - // delete labels with none existing repos - if _, err := db.GetEngine(db.DefaultContext). - Where(builder.And( - builder.Gt{"repo_id": 0}, - builder.NotIn("repo_id", builder.Select("id").From("repository")), - )). - Delete(Label{}); err != nil { - return err - } - - // delete labels with none existing orgs - if _, err := db.GetEngine(db.DefaultContext). - Where(builder.And( - builder.Gt{"org_id": 0}, - builder.NotIn("org_id", builder.Select("id").From("user")), - )). - Delete(Label{}); err != nil { - return err - } - - return nil -} - -// CountOrphanedIssueLabels return count of IssueLabels witch have no label behind anymore -func CountOrphanedIssueLabels() (int64, error) { - return db.GetEngine(db.DefaultContext).Table("issue_label"). - NotIn("label_id", builder.Select("id").From("label")). - Count() -} - -// DeleteOrphanedIssueLabels delete IssueLabels witch have no label behind anymore -func DeleteOrphanedIssueLabels() error { - _, err := db.GetEngine(db.DefaultContext). - NotIn("label_id", builder.Select("id").From("label")). - Delete(IssueLabel{}) - return err -} - -// CountOrphanedIssues count issues without a repo -func CountOrphanedIssues() (int64, error) { - return db.GetEngine(db.DefaultContext).Table("issue"). - Join("LEFT", "repository", "issue.repo_id=repository.id"). - Where(builder.IsNull{"repository.id"}). - Select("COUNT(`issue`.`id`)"). - Count() -} - -// DeleteOrphanedIssues delete issues without a repo -func DeleteOrphanedIssues() error { - ctx, committer, err := db.TxContext() - if err != nil { - return err - } - defer committer.Close() - - 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 - } - - var attachmentPaths []string - for i := range ids { - paths, err := deleteIssuesByRepoID(ctx, ids[i]) - if err != nil { - return err - } - attachmentPaths = append(attachmentPaths, paths...) - } - - if err := committer.Commit(); err != nil { - return err - } - committer.Close() - - // Remove issue attachment files. - for i := range attachmentPaths { - admin_model.RemoveAllWithNotice(db.DefaultContext, "Delete issue attachment", attachmentPaths[i]) - } - return nil -} - -// CountOrphanedObjects count subjects with have no existing refobject anymore -func CountOrphanedObjects(subject, refobject, joinCond string) (int64, error) { - return db.GetEngine(db.DefaultContext).Table("`"+subject+"`"). - Join("LEFT", "`"+refobject+"`", joinCond). - Where(builder.IsNull{"`" + refobject + "`.id"}). - Select("COUNT(`" + subject + "`.`id`)"). - Count() -} - -// DeleteOrphanedObjects delete subjects with have no existing refobject anymore -func DeleteOrphanedObjects(subject, refobject, joinCond string) error { - subQuery := builder.Select("`"+subject+"`.id"). - From("`"+subject+"`"). - Join("LEFT", "`"+refobject+"`", joinCond). - Where(builder.IsNull{"`" + refobject + "`.id"}) - sql, args, err := builder.Delete(builder.In("id", subQuery)).From("`" + subject + "`").ToSQL() - if err != nil { - return err - } - _, err = db.GetEngine(db.DefaultContext).Exec(append([]interface{}{sql}, args...)...) - return err -} - // CountNullArchivedRepository counts the number of repositories with is_archived is null func CountNullArchivedRepository() (int64, error) { return db.GetEngine(db.DefaultContext).Where(builder.IsNull{"is_archived"}).Count(new(repo_model.Repository)) @@ -181,74 +35,6 @@ func FixWrongUserType() (int64, error) { return db.GetEngine(db.DefaultContext).Where(builder.Eq{"type": 0}.And(builder.Neq{"num_teams": 0})).Cols("type").NoAutoTime().Update(&user_model.User{Type: 1}) } -// CountCommentTypeLabelWithEmptyLabel count label comments with empty label -func CountCommentTypeLabelWithEmptyLabel() (int64, error) { - return db.GetEngine(db.DefaultContext).Where(builder.Eq{"type": CommentTypeLabel, "label_id": 0}).Count(new(Comment)) -} - -// FixCommentTypeLabelWithEmptyLabel count label comments with empty label -func FixCommentTypeLabelWithEmptyLabel() (int64, error) { - return db.GetEngine(db.DefaultContext).Where(builder.Eq{"type": CommentTypeLabel, "label_id": 0}).Delete(new(Comment)) -} - -// CountCommentTypeLabelWithOutsideLabels count label comments with outside label -func CountCommentTypeLabelWithOutsideLabels() (int64, error) { - return db.GetEngine(db.DefaultContext).Where("comment.type = ? AND ((label.org_id = 0 AND issue.repo_id != label.repo_id) OR (label.repo_id = 0 AND label.org_id != repository.owner_id))", CommentTypeLabel). - Table("comment"). - Join("inner", "label", "label.id = comment.label_id"). - Join("inner", "issue", "issue.id = comment.issue_id "). - Join("inner", "repository", "issue.repo_id = repository.id"). - Count(new(Comment)) -} - -// FixCommentTypeLabelWithOutsideLabels count label comments with outside label -func FixCommentTypeLabelWithOutsideLabels() (int64, error) { - res, err := db.GetEngine(db.DefaultContext).Exec(`DELETE FROM comment WHERE comment.id IN ( - SELECT il_too.id FROM ( - SELECT com.id - FROM comment AS com - INNER JOIN label ON com.label_id = label.id - INNER JOIN issue on issue.id = com.issue_id - INNER JOIN repository ON issue.repo_id = repository.id - WHERE - com.type = ? AND ((label.org_id = 0 AND issue.repo_id != label.repo_id) OR (label.repo_id = 0 AND label.org_id != repository.owner_id)) - ) AS il_too)`, CommentTypeLabel) - if err != nil { - return 0, err - } - - return res.RowsAffected() -} - -// CountIssueLabelWithOutsideLabels count label comments with outside label -func CountIssueLabelWithOutsideLabels() (int64, error) { - return db.GetEngine(db.DefaultContext).Where(builder.Expr("(label.org_id = 0 AND issue.repo_id != label.repo_id) OR (label.repo_id = 0 AND label.org_id != repository.owner_id)")). - Table("issue_label"). - Join("inner", "label", "issue_label.label_id = label.id "). - Join("inner", "issue", "issue.id = issue_label.issue_id "). - Join("inner", "repository", "issue.repo_id = repository.id"). - Count(new(IssueLabel)) -} - -// FixIssueLabelWithOutsideLabels fix label comments with outside label -func FixIssueLabelWithOutsideLabels() (int64, error) { - res, err := db.GetEngine(db.DefaultContext).Exec(`DELETE FROM issue_label WHERE issue_label.id IN ( - SELECT il_too.id FROM ( - SELECT il_too_too.id - FROM issue_label AS il_too_too - INNER JOIN label ON il_too_too.label_id = label.id - INNER JOIN issue on issue.id = il_too_too.issue_id - INNER JOIN repository on repository.id = issue.repo_id - WHERE - (label.org_id = 0 AND issue.repo_id != label.repo_id) OR (label.repo_id = 0 AND label.org_id != repository.owner_id) - ) AS il_too )`) - if err != nil { - return 0, err - } - - return res.RowsAffected() -} - // CountActionCreatedUnixString count actions where created_unix is an empty string func CountActionCreatedUnixString() (int64, error) { if setting.Database.UseSQLite3 { diff --git a/models/consistency_test.go b/models/consistency_test.go deleted file mode 100644 index fb946b2fb7..0000000000 --- a/models/consistency_test.go +++ /dev/null @@ -1,149 +0,0 @@ -// Copyright 2021 Gitea. All rights reserved. -// Use of this source code is governed by a MIT-style -// license that can be found in the LICENSE file. - -package models - -import ( - "testing" - - "code.gitea.io/gitea/models/db" - issues_model "code.gitea.io/gitea/models/issues" - repo_model "code.gitea.io/gitea/models/repo" - "code.gitea.io/gitea/models/unittest" - "code.gitea.io/gitea/modules/setting" - "code.gitea.io/gitea/modules/timeutil" - - "github.com/stretchr/testify/assert" -) - -func TestDeleteOrphanedObjects(t *testing.T) { - assert.NoError(t, unittest.PrepareTestDatabase()) - - countBefore, err := db.GetEngine(db.DefaultContext).Count(&PullRequest{}) - assert.NoError(t, err) - - _, err = db.GetEngine(db.DefaultContext).Insert(&PullRequest{IssueID: 1000}, &PullRequest{IssueID: 1001}, &PullRequest{IssueID: 1003}) - assert.NoError(t, err) - - orphaned, err := CountOrphanedObjects("pull_request", "issue", "pull_request.issue_id=issue.id") - assert.NoError(t, err) - assert.EqualValues(t, 3, orphaned) - - err = DeleteOrphanedObjects("pull_request", "issue", "pull_request.issue_id=issue.id") - assert.NoError(t, err) - - countAfter, err := db.GetEngine(db.DefaultContext).Count(&PullRequest{}) - assert.NoError(t, err) - assert.EqualValues(t, countBefore, countAfter) -} - -func TestNewMilestone(t *testing.T) { - assert.NoError(t, unittest.PrepareTestDatabase()) - milestone := &issues_model.Milestone{ - RepoID: 1, - Name: "milestoneName", - Content: "milestoneContent", - } - - assert.NoError(t, issues_model.NewMilestone(milestone)) - unittest.AssertExistsAndLoadBean(t, milestone) - unittest.CheckConsistencyFor(t, &repo_model.Repository{ID: milestone.RepoID}, &issues_model.Milestone{}) -} - -func TestChangeMilestoneStatus(t *testing.T) { - assert.NoError(t, unittest.PrepareTestDatabase()) - milestone := unittest.AssertExistsAndLoadBean(t, &issues_model.Milestone{ID: 1}).(*issues_model.Milestone) - - assert.NoError(t, issues_model.ChangeMilestoneStatus(milestone, true)) - unittest.AssertExistsAndLoadBean(t, &issues_model.Milestone{ID: 1}, "is_closed=1") - unittest.CheckConsistencyFor(t, &repo_model.Repository{ID: milestone.RepoID}, &issues_model.Milestone{}) - - assert.NoError(t, issues_model.ChangeMilestoneStatus(milestone, false)) - unittest.AssertExistsAndLoadBean(t, &issues_model.Milestone{ID: 1}, "is_closed=0") - unittest.CheckConsistencyFor(t, &repo_model.Repository{ID: milestone.RepoID}, &issues_model.Milestone{}) -} - -func TestDeleteMilestoneByRepoID(t *testing.T) { - assert.NoError(t, unittest.PrepareTestDatabase()) - assert.NoError(t, issues_model.DeleteMilestoneByRepoID(1, 1)) - unittest.AssertNotExistsBean(t, &issues_model.Milestone{ID: 1}) - unittest.CheckConsistencyFor(t, &repo_model.Repository{ID: 1}) - - assert.NoError(t, issues_model.DeleteMilestoneByRepoID(unittest.NonexistentID, unittest.NonexistentID)) -} - -func TestUpdateMilestone(t *testing.T) { - assert.NoError(t, unittest.PrepareTestDatabase()) - - milestone := unittest.AssertExistsAndLoadBean(t, &issues_model.Milestone{ID: 1}).(*issues_model.Milestone) - milestone.Name = " newMilestoneName " - milestone.Content = "newMilestoneContent" - assert.NoError(t, issues_model.UpdateMilestone(milestone, milestone.IsClosed)) - milestone = unittest.AssertExistsAndLoadBean(t, &issues_model.Milestone{ID: 1}).(*issues_model.Milestone) - assert.EqualValues(t, "newMilestoneName", milestone.Name) - unittest.CheckConsistencyFor(t, &issues_model.Milestone{}) -} - -func TestUpdateMilestoneCounters(t *testing.T) { - assert.NoError(t, unittest.PrepareTestDatabase()) - issue := unittest.AssertExistsAndLoadBean(t, &Issue{MilestoneID: 1}, - "is_closed=0").(*Issue) - - issue.IsClosed = true - issue.ClosedUnix = timeutil.TimeStampNow() - _, err := db.GetEngine(db.DefaultContext).ID(issue.ID).Cols("is_closed", "closed_unix").Update(issue) - assert.NoError(t, err) - assert.NoError(t, issues_model.UpdateMilestoneCounters(db.DefaultContext, issue.MilestoneID)) - unittest.CheckConsistencyFor(t, &issues_model.Milestone{}) - - issue.IsClosed = false - issue.ClosedUnix = 0 - _, err = db.GetEngine(db.DefaultContext).ID(issue.ID).Cols("is_closed", "closed_unix").Update(issue) - assert.NoError(t, err) - assert.NoError(t, issues_model.UpdateMilestoneCounters(db.DefaultContext, issue.MilestoneID)) - unittest.CheckConsistencyFor(t, &issues_model.Milestone{}) -} - -func TestConsistencyUpdateAction(t *testing.T) { - if !setting.Database.UseSQLite3 { - t.Skip("Test is only for SQLite database.") - } - assert.NoError(t, unittest.PrepareTestDatabase()) - id := 8 - unittest.AssertExistsAndLoadBean(t, &Action{ - ID: int64(id), - }) - _, err := db.GetEngine(db.DefaultContext).Exec(`UPDATE action SET created_unix = "" WHERE id = ?`, id) - assert.NoError(t, err) - actions := make([]*Action, 0, 1) - // - // XORM returns an error when created_unix is a string - // - err = db.GetEngine(db.DefaultContext).Where("id = ?", id).Find(&actions) - if assert.Error(t, err) { - assert.Contains(t, err.Error(), "type string to a int64: invalid syntax") - } - // - // Get rid of incorrectly set created_unix - // - count, err := CountActionCreatedUnixString() - assert.NoError(t, err) - assert.EqualValues(t, 1, count) - count, err = FixActionCreatedUnixString() - assert.NoError(t, err) - assert.EqualValues(t, 1, count) - - count, err = CountActionCreatedUnixString() - assert.NoError(t, err) - assert.EqualValues(t, 0, count) - count, err = FixActionCreatedUnixString() - assert.NoError(t, err) - assert.EqualValues(t, 0, count) - - // - // XORM must be happy now - // - assert.NoError(t, db.GetEngine(db.DefaultContext).Where("id = ?", id).Find(&actions)) - unittest.CheckConsistencyFor(t, &Action{}) -} diff --git a/models/db/consistency.go b/models/db/consistency.go new file mode 100644 index 0000000000..7addb174c4 --- /dev/null +++ b/models/db/consistency.go @@ -0,0 +1,27 @@ +// Copyright 2022 The Gitea Authors. All rights reserved. +// Use of this source code is governed by a MIT-style +// license that can be found in the LICENSE file. + +package db + +import "xorm.io/builder" + +// CountOrphanedObjects count subjects with have no existing refobject anymore +func CountOrphanedObjects(subject, refobject, joinCond string) (int64, error) { + return GetEngine(DefaultContext).Table("`"+subject+"`"). + Join("LEFT", "`"+refobject+"`", joinCond). + Where(builder.IsNull{"`" + refobject + "`.id"}). + Select("COUNT(`" + subject + "`.`id`)"). + Count() +} + +// DeleteOrphanedObjects delete subjects with have no existing refobject anymore +func DeleteOrphanedObjects(subject, refobject, joinCond string) error { + subQuery := builder.Select("`"+subject+"`.id"). + From("`"+subject+"`"). + Join("LEFT", "`"+refobject+"`", joinCond). + Where(builder.IsNull{"`" + refobject + "`.id"}) + b := builder.Delete(builder.In("id", subQuery)).From("`" + subject + "`") + _, err := GetEngine(DefaultContext).Exec(b) + return err +} diff --git a/models/db/index.go b/models/db/index.go index 8598de9498..9b164db1fa 100644 --- a/models/db/index.go +++ b/models/db/index.go @@ -20,21 +20,21 @@ type ResourceIndex struct { } // UpsertResourceIndex the function will not return until it acquires the lock or receives an error. -func UpsertResourceIndex(e Engine, tableName string, groupID int64) (err error) { +func UpsertResourceIndex(ctx context.Context, tableName string, groupID int64) (err error) { // An atomic UPSERT operation (INSERT/UPDATE) is the only operation // that ensures that the key is actually locked. switch { case setting.Database.UseSQLite3 || setting.Database.UsePostgreSQL: - _, err = e.Exec(fmt.Sprintf("INSERT INTO %s (group_id, max_index) "+ + _, err = Exec(ctx, fmt.Sprintf("INSERT INTO %s (group_id, max_index) "+ "VALUES (?,1) ON CONFLICT (group_id) DO UPDATE SET max_index = %s.max_index+1", tableName, tableName), groupID) case setting.Database.UseMySQL: - _, err = e.Exec(fmt.Sprintf("INSERT INTO %s (group_id, max_index) "+ + _, err = Exec(ctx, fmt.Sprintf("INSERT INTO %s (group_id, max_index) "+ "VALUES (?,1) ON DUPLICATE KEY UPDATE max_index = max_index+1", tableName), groupID) case setting.Database.UseMSSQL: // https://weblogs.sqlteam.com/dang/2009/01/31/upsert-race-condition-with-merge/ - _, err = e.Exec(fmt.Sprintf("MERGE %s WITH (HOLDLOCK) as target "+ + _, err = Exec(ctx, fmt.Sprintf("MERGE %s WITH (HOLDLOCK) as target "+ "USING (SELECT ? AS group_id) AS src "+ "ON src.group_id = target.group_id "+ "WHEN MATCHED THEN UPDATE SET target.max_index = target.max_index+1 "+ @@ -82,30 +82,29 @@ func DeleteResouceIndex(ctx context.Context, tableName string, groupID int64) er // getNextResourceIndex return the next index func getNextResourceIndex(tableName string, groupID int64) (int64, error) { - sess := x.NewSession() - defer sess.Close() - if err := sess.Begin(); err != nil { + ctx, commiter, err := TxContext() + if err != nil { return 0, err } + defer commiter.Close() var preIdx int64 - _, err := sess.SQL(fmt.Sprintf("SELECT max_index FROM %s WHERE group_id = ?", tableName), groupID).Get(&preIdx) - if err != nil { + if _, err := GetEngine(ctx).SQL(fmt.Sprintf("SELECT max_index FROM %s WHERE group_id = ?", tableName), groupID).Get(&preIdx); err != nil { return 0, err } - if err := UpsertResourceIndex(sess, tableName, groupID); err != nil { + if err := UpsertResourceIndex(ctx, tableName, groupID); err != nil { return 0, err } var curIdx int64 - has, err := sess.SQL(fmt.Sprintf("SELECT max_index FROM %s WHERE group_id = ? AND max_index=?", tableName), groupID, preIdx+1).Get(&curIdx) + has, err := GetEngine(ctx).SQL(fmt.Sprintf("SELECT max_index FROM %s WHERE group_id = ? AND max_index=?", tableName), groupID, preIdx+1).Get(&curIdx) if err != nil { return 0, err } if !has { return 0, ErrResouceOutdated } - if err := sess.Commit(); err != nil { + if err := commiter.Commit(); err != nil { return 0, err } return curIdx, nil diff --git a/models/db/list_options.go b/models/db/list_options.go index 843e73c8ae..d1d52b6667 100644 --- a/models/db/list_options.go +++ b/models/db/list_options.go @@ -10,6 +10,11 @@ import ( "xorm.io/xorm" ) +const ( + // DefaultMaxInSize represents default variables number on IN () in SQL + DefaultMaxInSize = 50 +) + // Paginator is the base for different ListOptions types type Paginator interface { GetSkipTake() (skip, take int) diff --git a/models/error.go b/models/error.go index 16ae52fc43..3c617904f8 100644 --- a/models/error.go +++ b/models/error.go @@ -405,22 +405,6 @@ func (err ErrFilePathProtected) Error() string { return fmt.Sprintf("path is protected and can not be changed [path: %s]", err.Path) } -// ErrUserDoesNotHaveAccessToRepo represets an error where the user doesn't has access to a given repo. -type ErrUserDoesNotHaveAccessToRepo struct { - UserID int64 - RepoName string -} - -// IsErrUserDoesNotHaveAccessToRepo checks if an error is a ErrRepoFileAlreadyExists. -func IsErrUserDoesNotHaveAccessToRepo(err error) bool { - _, ok := err.(ErrUserDoesNotHaveAccessToRepo) - return ok -} - -func (err ErrUserDoesNotHaveAccessToRepo) Error() string { - return fmt.Sprintf("user doesn't have access to repo [user_id: %d, repo_name: %s]", err.UserID, err.RepoName) -} - // __________ .__ // \______ \____________ ____ ____ | |__ // | | _/\_ __ \__ \ / \_/ ___\| | \ @@ -580,162 +564,6 @@ func (err ErrSHAOrCommitIDNotProvided) Error() string { return "a SHA or commit ID must be proved when updating a file" } -// .___ -// | | ______ ________ __ ____ -// | |/ ___// ___/ | \_/ __ \ -// | |\___ \ \___ \| | /\ ___/ -// |___/____ >____ >____/ \___ > -// \/ \/ \/ - -// ErrIssueNotExist represents a "IssueNotExist" kind of error. -type ErrIssueNotExist struct { - ID int64 - RepoID int64 - Index int64 -} - -// IsErrIssueNotExist checks if an error is a ErrIssueNotExist. -func IsErrIssueNotExist(err error) bool { - _, ok := err.(ErrIssueNotExist) - return ok -} - -func (err ErrIssueNotExist) Error() string { - return fmt.Sprintf("issue does not exist [id: %d, repo_id: %d, index: %d]", err.ID, err.RepoID, err.Index) -} - -// ErrIssueIsClosed represents a "IssueIsClosed" kind of error. -type ErrIssueIsClosed struct { - ID int64 - RepoID int64 - Index int64 -} - -// IsErrIssueIsClosed checks if an error is a ErrIssueNotExist. -func IsErrIssueIsClosed(err error) bool { - _, ok := err.(ErrIssueIsClosed) - return ok -} - -func (err ErrIssueIsClosed) Error() string { - return fmt.Sprintf("issue is closed [id: %d, repo_id: %d, index: %d]", err.ID, err.RepoID, err.Index) -} - -// ErrNewIssueInsert is used when the INSERT statement in newIssue fails -type ErrNewIssueInsert struct { - OriginalError error -} - -// IsErrNewIssueInsert checks if an error is a ErrNewIssueInsert. -func IsErrNewIssueInsert(err error) bool { - _, ok := err.(ErrNewIssueInsert) - return ok -} - -func (err ErrNewIssueInsert) Error() string { - return err.OriginalError.Error() -} - -// ErrIssueWasClosed is used when close a closed issue -type ErrIssueWasClosed struct { - ID int64 - Index int64 -} - -// IsErrIssueWasClosed checks if an error is a ErrIssueWasClosed. -func IsErrIssueWasClosed(err error) bool { - _, ok := err.(ErrIssueWasClosed) - return ok -} - -func (err ErrIssueWasClosed) Error() string { - return fmt.Sprintf("Issue [%d] %d was already closed", err.ID, err.Index) -} - -// ErrPullWasClosed is used close a closed pull request -type ErrPullWasClosed struct { - ID int64 - Index int64 -} - -// IsErrPullWasClosed checks if an error is a ErrErrPullWasClosed. -func IsErrPullWasClosed(err error) bool { - _, ok := err.(ErrPullWasClosed) - return ok -} - -func (err ErrPullWasClosed) Error() string { - return fmt.Sprintf("Pull request [%d] %d was already closed", err.ID, err.Index) -} - -// __________ .__ .__ __________ __ -// \______ \__ __| | | |\______ \ ____ ________ __ ____ _______/ |_ -// | ___/ | \ | | | | _// __ \/ ____/ | \_/ __ \ / ___/\ __\ -// | | | | / |_| |_| | \ ___< <_| | | /\ ___/ \___ \ | | -// |____| |____/|____/____/____|_ /\___ >__ |____/ \___ >____ > |__| -// \/ \/ |__| \/ \/ - -// ErrPullRequestNotExist represents a "PullRequestNotExist" kind of error. -type ErrPullRequestNotExist struct { - ID int64 - IssueID int64 - HeadRepoID int64 - BaseRepoID int64 - HeadBranch string - BaseBranch string -} - -// IsErrPullRequestNotExist checks if an error is a ErrPullRequestNotExist. -func IsErrPullRequestNotExist(err error) bool { - _, ok := err.(ErrPullRequestNotExist) - return ok -} - -func (err ErrPullRequestNotExist) Error() string { - return fmt.Sprintf("pull request does not exist [id: %d, issue_id: %d, head_repo_id: %d, base_repo_id: %d, head_branch: %s, base_branch: %s]", - err.ID, err.IssueID, err.HeadRepoID, err.BaseRepoID, err.HeadBranch, err.BaseBranch) -} - -// ErrPullRequestAlreadyExists represents a "PullRequestAlreadyExists"-error -type ErrPullRequestAlreadyExists struct { - ID int64 - IssueID int64 - HeadRepoID int64 - BaseRepoID int64 - HeadBranch string - BaseBranch string -} - -// IsErrPullRequestAlreadyExists checks if an error is a ErrPullRequestAlreadyExists. -func IsErrPullRequestAlreadyExists(err error) bool { - _, ok := err.(ErrPullRequestAlreadyExists) - return ok -} - -// Error does pretty-printing :D -func (err ErrPullRequestAlreadyExists) Error() string { - return fmt.Sprintf("pull request already exists for these targets [id: %d, issue_id: %d, head_repo_id: %d, base_repo_id: %d, head_branch: %s, base_branch: %s]", - err.ID, err.IssueID, err.HeadRepoID, err.BaseRepoID, err.HeadBranch, err.BaseBranch) -} - -// ErrPullRequestHeadRepoMissing represents a "ErrPullRequestHeadRepoMissing" error -type ErrPullRequestHeadRepoMissing struct { - ID int64 - HeadRepoID int64 -} - -// IsErrErrPullRequestHeadRepoMissing checks if an error is a ErrPullRequestHeadRepoMissing. -func IsErrErrPullRequestHeadRepoMissing(err error) bool { - _, ok := err.(ErrPullRequestHeadRepoMissing) - return ok -} - -// Error does pretty-printing :D -func (err ErrPullRequestHeadRepoMissing) Error() string { - return fmt.Sprintf("pull request head repo missing [id: %d, head_repo_id: %d]", - err.ID, err.HeadRepoID) -} - // ErrInvalidMergeStyle represents an error if merging with disabled merge strategy type ErrInvalidMergeStyle struct { ID int64 @@ -830,29 +658,6 @@ func (err ErrPullRequestHasMerged) Error() string { err.ID, err.IssueID, err.HeadRepoID, err.BaseRepoID, err.HeadBranch, err.BaseBranch) } -// _________ __ -// \_ ___ \ ____ _____ _____ ____ _____/ |_ -// / \ \/ / _ \ / \ / \_/ __ \ / \ __\ -// \ \___( <_> ) Y Y \ Y Y \ ___/| | \ | -// \______ /\____/|__|_| /__|_| /\___ >___| /__| -// \/ \/ \/ \/ \/ - -// ErrCommentNotExist represents a "CommentNotExist" kind of error. -type ErrCommentNotExist struct { - ID int64 - IssueID int64 -} - -// IsErrCommentNotExist checks if an error is a ErrCommentNotExist. -func IsErrCommentNotExist(err error) bool { - _, ok := err.(ErrCommentNotExist) - return ok -} - -func (err ErrCommentNotExist) Error() string { - return fmt.Sprintf("comment does not exist [id: %d, issue_id: %d]", err.ID, err.IssueID) -} - // _________ __ __ .__ // / _____// |_ ____ ________ _ _______ _/ |_ ____ | |__ // \_____ \\ __\/ _ \\____ \ \/ \/ /\__ \\ __\/ ___\| | \ @@ -897,60 +702,6 @@ func (err ErrTrackedTimeNotExist) Error() string { return fmt.Sprintf("tracked time does not exist [id: %d]", err.ID) } -// .____ ___. .__ -// | | _____ \_ |__ ____ | | -// | | \__ \ | __ \_/ __ \| | -// | |___ / __ \| \_\ \ ___/| |__ -// |_______ (____ /___ /\___ >____/ -// \/ \/ \/ \/ - -// ErrRepoLabelNotExist represents a "RepoLabelNotExist" kind of error. -type ErrRepoLabelNotExist struct { - LabelID int64 - RepoID int64 -} - -// IsErrRepoLabelNotExist checks if an error is a RepoErrLabelNotExist. -func IsErrRepoLabelNotExist(err error) bool { - _, ok := err.(ErrRepoLabelNotExist) - return ok -} - -func (err ErrRepoLabelNotExist) Error() string { - return fmt.Sprintf("label does not exist [label_id: %d, repo_id: %d]", err.LabelID, err.RepoID) -} - -// ErrOrgLabelNotExist represents a "OrgLabelNotExist" kind of error. -type ErrOrgLabelNotExist struct { - LabelID int64 - OrgID int64 -} - -// IsErrOrgLabelNotExist checks if an error is a OrgErrLabelNotExist. -func IsErrOrgLabelNotExist(err error) bool { - _, ok := err.(ErrOrgLabelNotExist) - return ok -} - -func (err ErrOrgLabelNotExist) Error() string { - return fmt.Sprintf("label does not exist [label_id: %d, org_id: %d]", err.LabelID, err.OrgID) -} - -// ErrLabelNotExist represents a "LabelNotExist" kind of error. -type ErrLabelNotExist struct { - LabelID int64 -} - -// IsErrLabelNotExist checks if an error is a ErrLabelNotExist. -func IsErrLabelNotExist(err error) bool { - _, ok := err.(ErrLabelNotExist) - return ok -} - -func (err ErrLabelNotExist) Error() string { - return fmt.Sprintf("label does not exist [label_id: %d]", err.LabelID) -} - // ____ ___ .__ .___ // | | \______ | | _________ __| _/ // | | /\____ \| | / _ \__ \ / __ | @@ -974,130 +725,3 @@ func IsErrUploadNotExist(err error) bool { func (err ErrUploadNotExist) Error() string { return fmt.Sprintf("attachment does not exist [id: %d, uuid: %s]", err.ID, err.UUID) } - -// .___ ________ .___ .__ -// | | ______ ________ __ ____ \______ \ ____ ______ ____ ____ __| _/____ ____ ____ |__| ____ ______ -// | |/ ___// ___/ | \_/ __ \ | | \_/ __ \\____ \_/ __ \ / \ / __ |/ __ \ / \_/ ___\| |/ __ \ / ___/ -// | |\___ \ \___ \| | /\ ___/ | ` \ ___/| |_> > ___/| | \/ /_/ \ ___/| | \ \___| \ ___/ \___ \ -// |___/____ >____ >____/ \___ >_______ /\___ > __/ \___ >___| /\____ |\___ >___| /\___ >__|\___ >____ > -// \/ \/ \/ \/ \/|__| \/ \/ \/ \/ \/ \/ \/ \/ - -// ErrDependencyExists represents a "DependencyAlreadyExists" kind of error. -type ErrDependencyExists struct { - IssueID int64 - DependencyID int64 -} - -// IsErrDependencyExists checks if an error is a ErrDependencyExists. -func IsErrDependencyExists(err error) bool { - _, ok := err.(ErrDependencyExists) - return ok -} - -func (err ErrDependencyExists) Error() string { - return fmt.Sprintf("issue dependency does already exist [issue id: %d, dependency id: %d]", err.IssueID, err.DependencyID) -} - -// ErrDependencyNotExists represents a "DependencyAlreadyExists" kind of error. -type ErrDependencyNotExists struct { - IssueID int64 - DependencyID int64 -} - -// IsErrDependencyNotExists checks if an error is a ErrDependencyExists. -func IsErrDependencyNotExists(err error) bool { - _, ok := err.(ErrDependencyNotExists) - return ok -} - -func (err ErrDependencyNotExists) Error() string { - return fmt.Sprintf("issue dependency does not exist [issue id: %d, dependency id: %d]", err.IssueID, err.DependencyID) -} - -// ErrCircularDependency represents a "DependencyCircular" kind of error. -type ErrCircularDependency struct { - IssueID int64 - DependencyID int64 -} - -// IsErrCircularDependency checks if an error is a ErrCircularDependency. -func IsErrCircularDependency(err error) bool { - _, ok := err.(ErrCircularDependency) - return ok -} - -func (err ErrCircularDependency) Error() string { - return fmt.Sprintf("circular dependencies exists (two issues blocking each other) [issue id: %d, dependency id: %d]", err.IssueID, err.DependencyID) -} - -// ErrDependenciesLeft represents an error where the issue you're trying to close still has dependencies left. -type ErrDependenciesLeft struct { - IssueID int64 -} - -// IsErrDependenciesLeft checks if an error is a ErrDependenciesLeft. -func IsErrDependenciesLeft(err error) bool { - _, ok := err.(ErrDependenciesLeft) - return ok -} - -func (err ErrDependenciesLeft) Error() string { - return fmt.Sprintf("issue has open dependencies [issue id: %d]", err.IssueID) -} - -// ErrUnknownDependencyType represents an error where an unknown dependency type was passed -type ErrUnknownDependencyType struct { - Type DependencyType -} - -// IsErrUnknownDependencyType checks if an error is ErrUnknownDependencyType -func IsErrUnknownDependencyType(err error) bool { - _, ok := err.(ErrUnknownDependencyType) - return ok -} - -func (err ErrUnknownDependencyType) Error() string { - return fmt.Sprintf("unknown dependency type [type: %d]", err.Type) -} - -// __________ .__ -// \______ \ _______ _|__| ______ _ __ -// | _// __ \ \/ / |/ __ \ \/ \/ / -// | | \ ___/\ /| \ ___/\ / -// |____|_ /\___ >\_/ |__|\___ >\/\_/ -// \/ \/ \/ - -// ErrReviewNotExist represents a "ReviewNotExist" kind of error. -type ErrReviewNotExist struct { - ID int64 -} - -// IsErrReviewNotExist checks if an error is a ErrReviewNotExist. -func IsErrReviewNotExist(err error) bool { - _, ok := err.(ErrReviewNotExist) - return ok -} - -func (err ErrReviewNotExist) Error() string { - return fmt.Sprintf("review does not exist [id: %d]", err.ID) -} - -// ErrNotValidReviewRequest an not allowed review request modify -type ErrNotValidReviewRequest struct { - Reason string - UserID int64 - RepoID int64 -} - -// IsErrNotValidReviewRequest checks if an error is a ErrNotValidReviewRequest. -func IsErrNotValidReviewRequest(err error) bool { - _, ok := err.(ErrNotValidReviewRequest) - return ok -} - -func (err ErrNotValidReviewRequest) Error() string { - return fmt.Sprintf("%s [user_id: %d, repo_id: %d]", - err.Reason, - err.UserID, - err.RepoID) -} diff --git a/models/git/branches_test.go b/models/git/branches_test.go index 1e0b1a98b6..8102d28d48 100644 --- a/models/git/branches_test.go +++ b/models/git/branches_test.go @@ -7,9 +7,9 @@ package git_test import ( "testing" - "code.gitea.io/gitea/models" "code.gitea.io/gitea/models/db" git_model "code.gitea.io/gitea/models/git" + issues_model "code.gitea.io/gitea/models/issues" repo_model "code.gitea.io/gitea/models/repo" "code.gitea.io/gitea/models/unittest" @@ -120,10 +120,10 @@ func TestRenameBranch(t *testing.T) { repo1 = unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 1}).(*repo_model.Repository) assert.Equal(t, "main", repo1.DefaultBranch) - pull := unittest.AssertExistsAndLoadBean(t, &models.PullRequest{ID: 1}).(*models.PullRequest) // merged + pull := unittest.AssertExistsAndLoadBean(t, &issues_model.PullRequest{ID: 1}).(*issues_model.PullRequest) // merged assert.Equal(t, "master", pull.BaseBranch) - pull = unittest.AssertExistsAndLoadBean(t, &models.PullRequest{ID: 2}).(*models.PullRequest) // open + pull = unittest.AssertExistsAndLoadBean(t, &issues_model.PullRequest{ID: 2}).(*issues_model.PullRequest) // open assert.Equal(t, "main", pull.BaseBranch) renamedBranch := unittest.AssertExistsAndLoadBean(t, &git_model.RenamedBranch{ID: 2}).(*git_model.RenamedBranch) diff --git a/models/git/main_test.go b/models/git/main_test.go index 02401e5204..dc30dfaad7 100644 --- a/models/git/main_test.go +++ b/models/git/main_test.go @@ -8,6 +8,7 @@ import ( "path/filepath" "testing" + _ "code.gitea.io/gitea/models" "code.gitea.io/gitea/models/unittest" ) diff --git a/models/issue_label_test.go b/models/issue_label_test.go deleted file mode 100644 index 67a09151d8..0000000000 --- a/models/issue_label_test.go +++ /dev/null @@ -1,394 +0,0 @@ -// Copyright 2017 The Gitea Authors. All rights reserved. -// Use of this source code is governed by a MIT-style -// license that can be found in the LICENSE file. - -package models - -import ( - "html/template" - "testing" - - "code.gitea.io/gitea/models/db" - repo_model "code.gitea.io/gitea/models/repo" - "code.gitea.io/gitea/models/unittest" - user_model "code.gitea.io/gitea/models/user" - - "github.com/stretchr/testify/assert" -) - -// TODO TestGetLabelTemplateFile - -func TestLabel_CalOpenIssues(t *testing.T) { - assert.NoError(t, unittest.PrepareTestDatabase()) - label := unittest.AssertExistsAndLoadBean(t, &Label{ID: 1}).(*Label) - label.CalOpenIssues() - assert.EqualValues(t, 2, label.NumOpenIssues) -} - -func TestLabel_ForegroundColor(t *testing.T) { - assert.NoError(t, unittest.PrepareTestDatabase()) - label := unittest.AssertExistsAndLoadBean(t, &Label{ID: 1}).(*Label) - assert.Equal(t, template.CSS("#000"), label.ForegroundColor()) - - label = unittest.AssertExistsAndLoadBean(t, &Label{ID: 2}).(*Label) - assert.Equal(t, template.CSS("#fff"), label.ForegroundColor()) -} - -func TestNewLabels(t *testing.T) { - assert.NoError(t, unittest.PrepareTestDatabase()) - labels := []*Label{ - {RepoID: 2, Name: "labelName2", Color: "#123456"}, - {RepoID: 3, Name: "labelName3", Color: "#123"}, - {RepoID: 4, Name: "labelName4", Color: "ABCDEF"}, - {RepoID: 5, Name: "labelName5", Color: "DEF"}, - } - assert.Error(t, NewLabel(db.DefaultContext, &Label{RepoID: 3, Name: "invalid Color", Color: ""})) - assert.Error(t, NewLabel(db.DefaultContext, &Label{RepoID: 3, Name: "invalid Color", Color: "#45G"})) - assert.Error(t, NewLabel(db.DefaultContext, &Label{RepoID: 3, Name: "invalid Color", Color: "#12345G"})) - assert.Error(t, NewLabel(db.DefaultContext, &Label{RepoID: 3, Name: "invalid Color", Color: "45G"})) - assert.Error(t, NewLabel(db.DefaultContext, &Label{RepoID: 3, Name: "invalid Color", Color: "12345G"})) - for _, label := range labels { - unittest.AssertNotExistsBean(t, label) - } - assert.NoError(t, NewLabels(labels...)) - for _, label := range labels { - unittest.AssertExistsAndLoadBean(t, label, unittest.Cond("id = ?", label.ID)) - } - unittest.CheckConsistencyFor(t, &Label{}, &repo_model.Repository{}) -} - -func TestGetLabelByID(t *testing.T) { - assert.NoError(t, unittest.PrepareTestDatabase()) - label, err := GetLabelByID(db.DefaultContext, 1) - assert.NoError(t, err) - assert.EqualValues(t, 1, label.ID) - - _, err = GetLabelByID(db.DefaultContext, unittest.NonexistentID) - assert.True(t, IsErrLabelNotExist(err)) -} - -func TestGetLabelInRepoByName(t *testing.T) { - assert.NoError(t, unittest.PrepareTestDatabase()) - label, err := GetLabelInRepoByName(db.DefaultContext, 1, "label1") - assert.NoError(t, err) - assert.EqualValues(t, 1, label.ID) - assert.Equal(t, "label1", label.Name) - - _, err = GetLabelInRepoByName(db.DefaultContext, 1, "") - assert.True(t, IsErrRepoLabelNotExist(err)) - - _, err = GetLabelInRepoByName(db.DefaultContext, unittest.NonexistentID, "nonexistent") - assert.True(t, IsErrRepoLabelNotExist(err)) -} - -func TestGetLabelInRepoByNames(t *testing.T) { - assert.NoError(t, unittest.PrepareTestDatabase()) - labelIDs, err := GetLabelIDsInRepoByNames(1, []string{"label1", "label2"}) - assert.NoError(t, err) - - assert.Len(t, labelIDs, 2) - - assert.Equal(t, int64(1), labelIDs[0]) - assert.Equal(t, int64(2), labelIDs[1]) -} - -func TestGetLabelInRepoByNamesDiscardsNonExistentLabels(t *testing.T) { - assert.NoError(t, unittest.PrepareTestDatabase()) - // label3 doesn't exists.. See labels.yml - labelIDs, err := GetLabelIDsInRepoByNames(1, []string{"label1", "label2", "label3"}) - assert.NoError(t, err) - - assert.Len(t, labelIDs, 2) - - assert.Equal(t, int64(1), labelIDs[0]) - assert.Equal(t, int64(2), labelIDs[1]) - assert.NoError(t, err) -} - -func TestGetLabelInRepoByID(t *testing.T) { - assert.NoError(t, unittest.PrepareTestDatabase()) - label, err := GetLabelInRepoByID(db.DefaultContext, 1, 1) - assert.NoError(t, err) - assert.EqualValues(t, 1, label.ID) - - _, err = GetLabelInRepoByID(db.DefaultContext, 1, -1) - assert.True(t, IsErrRepoLabelNotExist(err)) - - _, err = GetLabelInRepoByID(db.DefaultContext, unittest.NonexistentID, unittest.NonexistentID) - assert.True(t, IsErrRepoLabelNotExist(err)) -} - -func TestGetLabelsInRepoByIDs(t *testing.T) { - assert.NoError(t, unittest.PrepareTestDatabase()) - labels, err := GetLabelsInRepoByIDs(1, []int64{1, 2, unittest.NonexistentID}) - assert.NoError(t, err) - if assert.Len(t, labels, 2) { - assert.EqualValues(t, 1, labels[0].ID) - assert.EqualValues(t, 2, labels[1].ID) - } -} - -func TestGetLabelsByRepoID(t *testing.T) { - assert.NoError(t, unittest.PrepareTestDatabase()) - testSuccess := func(repoID int64, sortType string, expectedIssueIDs []int64) { - labels, err := GetLabelsByRepoID(db.DefaultContext, repoID, sortType, db.ListOptions{}) - assert.NoError(t, err) - assert.Len(t, labels, len(expectedIssueIDs)) - for i, label := range labels { - assert.EqualValues(t, expectedIssueIDs[i], label.ID) - } - } - testSuccess(1, "leastissues", []int64{2, 1}) - testSuccess(1, "mostissues", []int64{1, 2}) - testSuccess(1, "reversealphabetically", []int64{2, 1}) - testSuccess(1, "default", []int64{1, 2}) -} - -// Org versions - -func TestGetLabelInOrgByName(t *testing.T) { - assert.NoError(t, unittest.PrepareTestDatabase()) - label, err := GetLabelInOrgByName(db.DefaultContext, 3, "orglabel3") - assert.NoError(t, err) - assert.EqualValues(t, 3, label.ID) - assert.Equal(t, "orglabel3", label.Name) - - _, err = GetLabelInOrgByName(db.DefaultContext, 3, "") - assert.True(t, IsErrOrgLabelNotExist(err)) - - _, err = GetLabelInOrgByName(db.DefaultContext, 0, "orglabel3") - assert.True(t, IsErrOrgLabelNotExist(err)) - - _, err = GetLabelInOrgByName(db.DefaultContext, -1, "orglabel3") - assert.True(t, IsErrOrgLabelNotExist(err)) - - _, err = GetLabelInOrgByName(db.DefaultContext, unittest.NonexistentID, "nonexistent") - assert.True(t, IsErrOrgLabelNotExist(err)) -} - -func TestGetLabelInOrgByNames(t *testing.T) { - assert.NoError(t, unittest.PrepareTestDatabase()) - labelIDs, err := GetLabelIDsInOrgByNames(3, []string{"orglabel3", "orglabel4"}) - assert.NoError(t, err) - - assert.Len(t, labelIDs, 2) - - assert.Equal(t, int64(3), labelIDs[0]) - assert.Equal(t, int64(4), labelIDs[1]) -} - -func TestGetLabelInOrgByNamesDiscardsNonExistentLabels(t *testing.T) { - assert.NoError(t, unittest.PrepareTestDatabase()) - // orglabel99 doesn't exists.. See labels.yml - labelIDs, err := GetLabelIDsInOrgByNames(3, []string{"orglabel3", "orglabel4", "orglabel99"}) - assert.NoError(t, err) - - assert.Len(t, labelIDs, 2) - - assert.Equal(t, int64(3), labelIDs[0]) - assert.Equal(t, int64(4), labelIDs[1]) - assert.NoError(t, err) -} - -func TestGetLabelInOrgByID(t *testing.T) { - assert.NoError(t, unittest.PrepareTestDatabase()) - label, err := GetLabelInOrgByID(db.DefaultContext, 3, 3) - assert.NoError(t, err) - assert.EqualValues(t, 3, label.ID) - - _, err = GetLabelInOrgByID(db.DefaultContext, 3, -1) - assert.True(t, IsErrOrgLabelNotExist(err)) - - _, err = GetLabelInOrgByID(db.DefaultContext, 0, 3) - assert.True(t, IsErrOrgLabelNotExist(err)) - - _, err = GetLabelInOrgByID(db.DefaultContext, -1, 3) - assert.True(t, IsErrOrgLabelNotExist(err)) - - _, err = GetLabelInOrgByID(db.DefaultContext, unittest.NonexistentID, unittest.NonexistentID) - assert.True(t, IsErrOrgLabelNotExist(err)) -} - -func TestGetLabelsInOrgByIDs(t *testing.T) { - assert.NoError(t, unittest.PrepareTestDatabase()) - labels, err := GetLabelsInOrgByIDs(3, []int64{3, 4, unittest.NonexistentID}) - assert.NoError(t, err) - if assert.Len(t, labels, 2) { - assert.EqualValues(t, 3, labels[0].ID) - assert.EqualValues(t, 4, labels[1].ID) - } -} - -func TestGetLabelsByOrgID(t *testing.T) { - assert.NoError(t, unittest.PrepareTestDatabase()) - testSuccess := func(orgID int64, sortType string, expectedIssueIDs []int64) { - labels, err := GetLabelsByOrgID(db.DefaultContext, orgID, sortType, db.ListOptions{}) - assert.NoError(t, err) - assert.Len(t, labels, len(expectedIssueIDs)) - for i, label := range labels { - assert.EqualValues(t, expectedIssueIDs[i], label.ID) - } - } - testSuccess(3, "leastissues", []int64{3, 4}) - testSuccess(3, "mostissues", []int64{4, 3}) - testSuccess(3, "reversealphabetically", []int64{4, 3}) - testSuccess(3, "default", []int64{3, 4}) - - var err error - _, err = GetLabelsByOrgID(db.DefaultContext, 0, "leastissues", db.ListOptions{}) - assert.True(t, IsErrOrgLabelNotExist(err)) - - _, err = GetLabelsByOrgID(db.DefaultContext, -1, "leastissues", db.ListOptions{}) - assert.True(t, IsErrOrgLabelNotExist(err)) -} - -// - -func TestGetLabelsByIssueID(t *testing.T) { - assert.NoError(t, unittest.PrepareTestDatabase()) - labels, err := GetLabelsByIssueID(db.DefaultContext, 1) - assert.NoError(t, err) - if assert.Len(t, labels, 1) { - assert.EqualValues(t, 1, labels[0].ID) - } - - labels, err = GetLabelsByIssueID(db.DefaultContext, unittest.NonexistentID) - assert.NoError(t, err) - assert.Len(t, labels, 0) -} - -func TestUpdateLabel(t *testing.T) { - assert.NoError(t, unittest.PrepareTestDatabase()) - label := unittest.AssertExistsAndLoadBean(t, &Label{ID: 1}).(*Label) - // make sure update wont overwrite it - update := &Label{ - ID: label.ID, - Color: "#ffff00", - Name: "newLabelName", - Description: label.Description, - } - label.Color = update.Color - label.Name = update.Name - assert.NoError(t, UpdateLabel(update)) - newLabel := unittest.AssertExistsAndLoadBean(t, &Label{ID: 1}).(*Label) - 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) - unittest.CheckConsistencyFor(t, &Label{}, &repo_model.Repository{}) -} - -func TestDeleteLabel(t *testing.T) { - assert.NoError(t, unittest.PrepareTestDatabase()) - label := unittest.AssertExistsAndLoadBean(t, &Label{ID: 1}).(*Label) - assert.NoError(t, DeleteLabel(label.RepoID, label.ID)) - unittest.AssertNotExistsBean(t, &Label{ID: label.ID, RepoID: label.RepoID}) - - assert.NoError(t, DeleteLabel(label.RepoID, label.ID)) - unittest.AssertNotExistsBean(t, &Label{ID: label.ID}) - - assert.NoError(t, DeleteLabel(unittest.NonexistentID, unittest.NonexistentID)) - unittest.CheckConsistencyFor(t, &Label{}, &repo_model.Repository{}) -} - -func TestHasIssueLabel(t *testing.T) { - assert.NoError(t, unittest.PrepareTestDatabase()) - assert.True(t, HasIssueLabel(db.DefaultContext, 1, 1)) - assert.False(t, HasIssueLabel(db.DefaultContext, 1, 2)) - assert.False(t, HasIssueLabel(db.DefaultContext, unittest.NonexistentID, unittest.NonexistentID)) -} - -func TestNewIssueLabel(t *testing.T) { - assert.NoError(t, unittest.PrepareTestDatabase()) - label := unittest.AssertExistsAndLoadBean(t, &Label{ID: 2}).(*Label) - issue := unittest.AssertExistsAndLoadBean(t, &Issue{ID: 1}).(*Issue) - doer := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2}).(*user_model.User) - - // add new IssueLabel - prevNumIssues := label.NumIssues - assert.NoError(t, NewIssueLabel(issue, label, doer)) - unittest.AssertExistsAndLoadBean(t, &IssueLabel{IssueID: issue.ID, LabelID: label.ID}) - unittest.AssertExistsAndLoadBean(t, &Comment{ - Type: CommentTypeLabel, - PosterID: doer.ID, - IssueID: issue.ID, - LabelID: label.ID, - Content: "1", - }) - label = unittest.AssertExistsAndLoadBean(t, &Label{ID: 2}).(*Label) - assert.EqualValues(t, prevNumIssues+1, label.NumIssues) - - // re-add existing IssueLabel - assert.NoError(t, NewIssueLabel(issue, label, doer)) - unittest.CheckConsistencyFor(t, &Issue{}, &Label{}) -} - -func TestNewIssueLabels(t *testing.T) { - assert.NoError(t, unittest.PrepareTestDatabase()) - label1 := unittest.AssertExistsAndLoadBean(t, &Label{ID: 1}).(*Label) - label2 := unittest.AssertExistsAndLoadBean(t, &Label{ID: 2}).(*Label) - issue := unittest.AssertExistsAndLoadBean(t, &Issue{ID: 5}).(*Issue) - doer := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2}).(*user_model.User) - - assert.NoError(t, NewIssueLabels(issue, []*Label{label1, label2}, doer)) - unittest.AssertExistsAndLoadBean(t, &IssueLabel{IssueID: issue.ID, LabelID: label1.ID}) - unittest.AssertExistsAndLoadBean(t, &Comment{ - Type: CommentTypeLabel, - PosterID: doer.ID, - IssueID: issue.ID, - LabelID: label1.ID, - Content: "1", - }) - unittest.AssertExistsAndLoadBean(t, &IssueLabel{IssueID: issue.ID, LabelID: label1.ID}) - label1 = unittest.AssertExistsAndLoadBean(t, &Label{ID: 1}).(*Label) - assert.EqualValues(t, 3, label1.NumIssues) - assert.EqualValues(t, 1, label1.NumClosedIssues) - label2 = unittest.AssertExistsAndLoadBean(t, &Label{ID: 2}).(*Label) - assert.EqualValues(t, 1, label2.NumIssues) - assert.EqualValues(t, 1, label2.NumClosedIssues) - - // corner case: test empty slice - assert.NoError(t, NewIssueLabels(issue, []*Label{}, doer)) - - unittest.CheckConsistencyFor(t, &Issue{}, &Label{}) -} - -func TestDeleteIssueLabel(t *testing.T) { - assert.NoError(t, unittest.PrepareTestDatabase()) - testSuccess := func(labelID, issueID, doerID int64) { - label := unittest.AssertExistsAndLoadBean(t, &Label{ID: labelID}).(*Label) - issue := unittest.AssertExistsAndLoadBean(t, &Issue{ID: issueID}).(*Issue) - doer := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: doerID}).(*user_model.User) - - expectedNumIssues := label.NumIssues - expectedNumClosedIssues := label.NumClosedIssues - if unittest.BeanExists(t, &IssueLabel{IssueID: issueID, LabelID: labelID}) { - expectedNumIssues-- - if issue.IsClosed { - expectedNumClosedIssues-- - } - } - - ctx, committer, err := db.TxContext() - defer committer.Close() - assert.NoError(t, err) - assert.NoError(t, DeleteIssueLabel(ctx, issue, label, doer)) - assert.NoError(t, committer.Commit()) - - unittest.AssertNotExistsBean(t, &IssueLabel{IssueID: issueID, LabelID: labelID}) - unittest.AssertExistsAndLoadBean(t, &Comment{ - Type: CommentTypeLabel, - PosterID: doerID, - IssueID: issueID, - LabelID: labelID, - }, `content=""`) - label = unittest.AssertExistsAndLoadBean(t, &Label{ID: labelID}).(*Label) - assert.EqualValues(t, expectedNumIssues, label.NumIssues) - assert.EqualValues(t, expectedNumClosedIssues, label.NumClosedIssues) - } - testSuccess(1, 1, 2) - testSuccess(2, 5, 2) - testSuccess(1, 1, 2) // delete non-existent IssueLabel - - unittest.CheckConsistencyFor(t, &Issue{}, &Label{}) -} diff --git a/models/issue_stopwatch_test.go b/models/issue_stopwatch_test.go deleted file mode 100644 index 15d5f234fd..0000000000 --- a/models/issue_stopwatch_test.go +++ /dev/null @@ -1,78 +0,0 @@ -// Copyright 2020 The Gitea Authors. All rights reserved. -// Use of this source code is governed by a MIT-style -// license that can be found in the LICENSE file. - -package models - -import ( - "testing" - - "code.gitea.io/gitea/models/db" - "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" -) - -func TestCancelStopwatch(t *testing.T) { - assert.NoError(t, unittest.PrepareTestDatabase()) - - user1, err := user_model.GetUserByID(1) - assert.NoError(t, err) - - issue1, err := GetIssueByID(1) - assert.NoError(t, err) - issue2, err := GetIssueByID(2) - assert.NoError(t, err) - - err = CancelStopwatch(user1, issue1) - assert.NoError(t, err) - unittest.AssertNotExistsBean(t, &Stopwatch{UserID: user1.ID, IssueID: issue1.ID}) - - _ = unittest.AssertExistsAndLoadBean(t, &Comment{Type: CommentTypeCancelTracking, PosterID: user1.ID, IssueID: issue1.ID}) - - assert.Nil(t, CancelStopwatch(user1, issue2)) -} - -func TestStopwatchExists(t *testing.T) { - assert.NoError(t, unittest.PrepareTestDatabase()) - - assert.True(t, StopwatchExists(1, 1)) - assert.False(t, StopwatchExists(1, 2)) -} - -func TestHasUserStopwatch(t *testing.T) { - assert.NoError(t, unittest.PrepareTestDatabase()) - - exists, sw, err := HasUserStopwatch(db.DefaultContext, 1) - assert.NoError(t, err) - assert.True(t, exists) - assert.Equal(t, int64(1), sw.ID) - - exists, _, err = HasUserStopwatch(db.DefaultContext, 3) - assert.NoError(t, err) - assert.False(t, exists) -} - -func TestCreateOrStopIssueStopwatch(t *testing.T) { - assert.NoError(t, unittest.PrepareTestDatabase()) - - user2, err := user_model.GetUserByID(2) - assert.NoError(t, err) - user3, err := user_model.GetUserByID(3) - assert.NoError(t, err) - - issue1, err := GetIssueByID(1) - assert.NoError(t, err) - issue2, err := GetIssueByID(2) - assert.NoError(t, err) - - assert.NoError(t, CreateOrStopIssueStopwatch(user3, issue1)) - sw := unittest.AssertExistsAndLoadBean(t, &Stopwatch{UserID: 3, IssueID: 1}).(*Stopwatch) - assert.LessOrEqual(t, sw.CreatedUnix, timeutil.TimeStampNow()) - - assert.NoError(t, CreateOrStopIssueStopwatch(user2, issue2)) - unittest.AssertNotExistsBean(t, &Stopwatch{UserID: 2, IssueID: 2}) - unittest.AssertExistsAndLoadBean(t, &TrackedTime{UserID: 2, IssueID: 2}) -} diff --git a/models/issue_user_test.go b/models/issue_user_test.go deleted file mode 100644 index 946da6e18d..0000000000 --- a/models/issue_user_test.go +++ /dev/null @@ -1,61 +0,0 @@ -// Copyright 2017 The Gogs Authors. All rights reserved. -// Use of this source code is governed by a MIT-style -// license that can be found in the LICENSE file. - -package models - -import ( - "testing" - - "code.gitea.io/gitea/models/db" - repo_model "code.gitea.io/gitea/models/repo" - "code.gitea.io/gitea/models/unittest" - - "github.com/stretchr/testify/assert" -) - -func Test_newIssueUsers(t *testing.T) { - assert.NoError(t, unittest.PrepareTestDatabase()) - - repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 1}).(*repo_model.Repository) - newIssue := &Issue{ - RepoID: repo.ID, - PosterID: 4, - Index: 6, - Title: "newTestIssueTitle", - Content: "newTestIssueContent", - } - - // artificially insert new issue - unittest.AssertSuccessfulInsert(t, newIssue) - - assert.NoError(t, newIssueUsers(db.DefaultContext, repo, newIssue)) - - // issue_user table should now have entries for new issue - unittest.AssertExistsAndLoadBean(t, &IssueUser{IssueID: newIssue.ID, UID: newIssue.PosterID}) - unittest.AssertExistsAndLoadBean(t, &IssueUser{IssueID: newIssue.ID, UID: repo.OwnerID}) -} - -func TestUpdateIssueUserByRead(t *testing.T) { - assert.NoError(t, unittest.PrepareTestDatabase()) - issue := unittest.AssertExistsAndLoadBean(t, &Issue{ID: 1}).(*Issue) - - assert.NoError(t, UpdateIssueUserByRead(4, issue.ID)) - unittest.AssertExistsAndLoadBean(t, &IssueUser{IssueID: issue.ID, UID: 4}, "is_read=1") - - assert.NoError(t, UpdateIssueUserByRead(4, issue.ID)) - unittest.AssertExistsAndLoadBean(t, &IssueUser{IssueID: issue.ID, UID: 4}, "is_read=1") - - assert.NoError(t, UpdateIssueUserByRead(unittest.NonexistentID, unittest.NonexistentID)) -} - -func TestUpdateIssueUsersByMentions(t *testing.T) { - assert.NoError(t, unittest.PrepareTestDatabase()) - issue := unittest.AssertExistsAndLoadBean(t, &Issue{ID: 1}).(*Issue) - - uids := []int64{2, 5} - assert.NoError(t, UpdateIssueUsersByMentions(db.DefaultContext, issue.ID, uids)) - for _, uid := range uids { - unittest.AssertExistsAndLoadBean(t, &IssueUser{IssueID: issue.ID, UID: uid}, "is_mentioned=1") - } -} diff --git a/models/issue_assignees.go b/models/issues/assignees.go index c6ccb6e9d2..5921112fea 100644 --- a/models/issue_assignees.go +++ b/models/issues/assignees.go @@ -2,7 +2,7 @@ // Use of this source code is governed by a MIT-style // license that can be found in the LICENSE file. -package models +package issues import ( "context" diff --git a/models/issue_assignees_test.go b/models/issues/assignees_test.go index 80317e1604..37d966f140 100644 --- a/models/issue_assignees_test.go +++ b/models/issues/assignees_test.go @@ -2,12 +2,13 @@ // Use of this source code is governed by a MIT-style // license that can be found in the LICENSE file. -package models +package issues_test import ( "testing" "code.gitea.io/gitea/models/db" + issues_model "code.gitea.io/gitea/models/issues" "code.gitea.io/gitea/models/unittest" user_model "code.gitea.io/gitea/models/user" @@ -18,27 +19,27 @@ func TestUpdateAssignee(t *testing.T) { assert.NoError(t, unittest.PrepareTestDatabase()) // Fake issue with assignees - issue, err := GetIssueWithAttrsByID(1) + issue, err := issues_model.GetIssueWithAttrsByID(1) assert.NoError(t, err) // Assign multiple users user2, err := user_model.GetUserByID(2) assert.NoError(t, err) - _, _, err = ToggleIssueAssignee(issue, &user_model.User{ID: 1}, user2.ID) + _, _, err = issues_model.ToggleIssueAssignee(issue, &user_model.User{ID: 1}, user2.ID) assert.NoError(t, err) user3, err := user_model.GetUserByID(3) assert.NoError(t, err) - _, _, err = ToggleIssueAssignee(issue, &user_model.User{ID: 1}, user3.ID) + _, _, err = issues_model.ToggleIssueAssignee(issue, &user_model.User{ID: 1}, user3.ID) assert.NoError(t, err) user1, err := user_model.GetUserByID(1) // This user is already assigned (see the definition in fixtures), so running UpdateAssignee should unassign him assert.NoError(t, err) - _, _, err = ToggleIssueAssignee(issue, &user_model.User{ID: 1}, user1.ID) + _, _, err = issues_model.ToggleIssueAssignee(issue, &user_model.User{ID: 1}, user1.ID) assert.NoError(t, err) // Check if he got removed - isAssigned, err := IsUserAssignedToIssue(db.DefaultContext, issue, user1) + isAssigned, err := issues_model.IsUserAssignedToIssue(db.DefaultContext, issue, user1) assert.NoError(t, err) assert.False(t, isAssigned) @@ -54,12 +55,12 @@ func TestUpdateAssignee(t *testing.T) { } // Check if the user is assigned - isAssigned, err = IsUserAssignedToIssue(db.DefaultContext, issue, user2) + isAssigned, err = issues_model.IsUserAssignedToIssue(db.DefaultContext, issue, user2) assert.NoError(t, err) assert.True(t, isAssigned) // This user should not be assigned - isAssigned, err = IsUserAssignedToIssue(db.DefaultContext, issue, &user_model.User{ID: 4}) + isAssigned, err = issues_model.IsUserAssignedToIssue(db.DefaultContext, issue, &user_model.User{ID: 4}) assert.NoError(t, err) assert.False(t, isAssigned) } @@ -70,22 +71,22 @@ func TestMakeIDsFromAPIAssigneesToAdd(t *testing.T) { _ = unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 1}).(*user_model.User) _ = unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2}).(*user_model.User) - IDs, err := MakeIDsFromAPIAssigneesToAdd("", []string{""}) + IDs, err := issues_model.MakeIDsFromAPIAssigneesToAdd("", []string{""}) assert.NoError(t, err) assert.Equal(t, []int64{}, IDs) - _, err = MakeIDsFromAPIAssigneesToAdd("", []string{"none_existing_user"}) + _, err = issues_model.MakeIDsFromAPIAssigneesToAdd("", []string{"none_existing_user"}) assert.Error(t, err) - IDs, err = MakeIDsFromAPIAssigneesToAdd("user1", []string{"user1"}) + IDs, err = issues_model.MakeIDsFromAPIAssigneesToAdd("user1", []string{"user1"}) assert.NoError(t, err) assert.Equal(t, []int64{1}, IDs) - IDs, err = MakeIDsFromAPIAssigneesToAdd("user2", []string{""}) + IDs, err = issues_model.MakeIDsFromAPIAssigneesToAdd("user2", []string{""}) assert.NoError(t, err) assert.Equal(t, []int64{2}, IDs) - IDs, err = MakeIDsFromAPIAssigneesToAdd("", []string{"user1", "user2"}) + IDs, err = issues_model.MakeIDsFromAPIAssigneesToAdd("", []string{"user1", "user2"}) assert.NoError(t, err) assert.Equal(t, []int64{1, 2}, IDs) } diff --git a/models/issue_comment.go b/models/issues/comment.go index 21cd87108d..a4e69e7118 100644 --- a/models/issue_comment.go +++ b/models/issues/comment.go @@ -4,7 +4,7 @@ // Use of this source code is governed by a MIT-style // license that can be found in the LICENSE file. -package models +package issues import ( "context" @@ -16,7 +16,6 @@ import ( "code.gitea.io/gitea/models/db" git_model "code.gitea.io/gitea/models/git" - issues_model "code.gitea.io/gitea/models/issues" "code.gitea.io/gitea/models/organization" project_model "code.gitea.io/gitea/models/project" repo_model "code.gitea.io/gitea/models/repo" @@ -34,6 +33,22 @@ import ( "xorm.io/xorm" ) +// ErrCommentNotExist represents a "CommentNotExist" kind of error. +type ErrCommentNotExist struct { + ID int64 + IssueID int64 +} + +// IsErrCommentNotExist checks if an error is a ErrCommentNotExist. +func IsErrCommentNotExist(err error) bool { + _, ok := err.(ErrCommentNotExist) + return ok +} + +func (err ErrCommentNotExist) Error() string { + return fmt.Sprintf("comment does not exist [id: %d, issue_id: %d]", err.ID, err.IssueID) +} + // CommentType defines whether a comment is just a simple comment, an action (like close) or a reference. type CommentType int @@ -216,8 +231,8 @@ type Comment struct { Project *project_model.Project `xorm:"-"` OldMilestoneID int64 MilestoneID int64 - OldMilestone *issues_model.Milestone `xorm:"-"` - Milestone *issues_model.Milestone `xorm:"-"` + OldMilestone *Milestone `xorm:"-"` + Milestone *Milestone `xorm:"-"` TimeID int64 Time *TrackedTime `xorm:"-"` AssigneeID int64 @@ -250,8 +265,8 @@ type Comment struct { // Reference issue in commit message CommitSHA string `xorm:"VARCHAR(40)"` - Attachments []*repo_model.Attachment `xorm:"-"` - Reactions issues_model.ReactionList `xorm:"-"` + Attachments []*repo_model.Attachment `xorm:"-"` + Reactions ReactionList `xorm:"-"` // For view issue page. ShowRole RoleDescriptor `xorm:"-"` @@ -299,7 +314,7 @@ func (c *Comment) LoadIssueCtx(ctx context.Context) (err error) { if c.Issue != nil { return nil } - c.Issue, err = getIssueByID(ctx, c.IssueID) + c.Issue, err = GetIssueByID(ctx, c.IssueID) return } @@ -503,7 +518,7 @@ func (c *Comment) LoadProject() error { // LoadMilestone if comment.Type is CommentTypeMilestone, then load milestone func (c *Comment) LoadMilestone() error { if c.OldMilestoneID > 0 { - var oldMilestone issues_model.Milestone + var oldMilestone Milestone has, err := db.GetEngine(db.DefaultContext).ID(c.OldMilestoneID).Get(&oldMilestone) if err != nil { return err @@ -513,7 +528,7 @@ func (c *Comment) LoadMilestone() error { } if c.MilestoneID > 0 { - var milestone issues_model.Milestone + var milestone Milestone has, err := db.GetEngine(db.DefaultContext).ID(c.MilestoneID).Get(&milestone) if err != nil { return err @@ -625,7 +640,7 @@ func (c *Comment) LoadDepIssueDetails() (err error) { if c.DependentIssueID <= 0 || c.DependentIssue != nil { return nil } - c.DependentIssue, err = getIssueByID(db.DefaultContext, c.DependentIssueID) + c.DependentIssue, err = GetIssueByID(db.DefaultContext, c.DependentIssueID) return err } @@ -643,7 +658,7 @@ func (c *Comment) loadReactions(ctx context.Context, repo *repo_model.Repository if c.Reactions != nil { return nil } - c.Reactions, _, err = issues_model.FindReactions(ctx, issues_model.FindReactionsOptions{ + c.Reactions, _, err = FindReactions(ctx, FindReactionsOptions{ IssueID: c.IssueID, CommentID: c.ID, }) @@ -823,7 +838,7 @@ func CreateCommentCtx(ctx context.Context, opts *CreateCommentOptions) (_ *Comme return nil, err } - if err = comment.addCrossReferences(ctx, opts.Doer, false); err != nil { + if err = comment.AddCrossReferences(ctx, opts.Doer, false); err != nil { return nil, err } @@ -1128,7 +1143,7 @@ func UpdateComment(c *Comment, doer *user_model.User) error { if err := c.LoadIssueCtx(ctx); err != nil { return err } - if err := c.addCrossReferences(ctx, doer, true); err != nil { + if err := c.AddCrossReferences(ctx, doer, true); err != nil { return err } if err := committer.Commit(); err != nil { @@ -1139,27 +1154,13 @@ func UpdateComment(c *Comment, doer *user_model.User) error { } // DeleteComment deletes the comment -func DeleteComment(comment *Comment) error { - ctx, committer, err := db.TxContext() - if err != nil { - return err - } - defer committer.Close() - - if err := deleteComment(ctx, comment); err != nil { - return err - } - - return committer.Commit() -} - -func deleteComment(ctx context.Context, comment *Comment) error { +func DeleteComment(ctx context.Context, comment *Comment) error { e := db.GetEngine(ctx) if _, err := e.ID(comment.ID).NoAutoCondition().Delete(comment); err != nil { return err } - if _, err := db.DeleteByBean(ctx, &issues_model.ContentHistory{ + if _, err := db.DeleteByBean(ctx, &ContentHistory{ CommentID: comment.ID, }); err != nil { return err @@ -1170,7 +1171,11 @@ func deleteComment(ctx context.Context, comment *Comment) error { return err } } - if _, err := e.Where("comment_id = ?", comment.ID).Cols("is_deleted").Update(&Action{IsDeleted: true}); err != nil { + if _, err := e.Table("action"). + Where("comment_id = ?", comment.ID). + Update(map[string]interface{}{ + "is_deleted": true, + }); err != nil { return err } @@ -1178,7 +1183,7 @@ func deleteComment(ctx context.Context, comment *Comment) error { return err } - return issues_model.DeleteReaction(ctx, &issues_model.ReactionOptions{CommentID: comment.ID}) + return DeleteReaction(ctx, &ReactionOptions{CommentID: comment.ID}) } // CodeComments represents comments on code by using this structure: FILENAME -> LINE (+ == proposed; - == previous) -> COMMENTS @@ -1500,3 +1505,42 @@ func (c *Comment) GetExternalName() string { return c.OriginalAuthor } // GetExternalID ExternalUserRemappable interface func (c *Comment) GetExternalID() int64 { return c.OriginalAuthorID } + +// CountCommentTypeLabelWithEmptyLabel count label comments with empty label +func CountCommentTypeLabelWithEmptyLabel() (int64, error) { + return db.GetEngine(db.DefaultContext).Where(builder.Eq{"type": CommentTypeLabel, "label_id": 0}).Count(new(Comment)) +} + +// FixCommentTypeLabelWithEmptyLabel count label comments with empty label +func FixCommentTypeLabelWithEmptyLabel() (int64, error) { + return db.GetEngine(db.DefaultContext).Where(builder.Eq{"type": CommentTypeLabel, "label_id": 0}).Delete(new(Comment)) +} + +// CountCommentTypeLabelWithOutsideLabels count label comments with outside label +func CountCommentTypeLabelWithOutsideLabels() (int64, error) { + return db.GetEngine(db.DefaultContext).Where("comment.type = ? AND ((label.org_id = 0 AND issue.repo_id != label.repo_id) OR (label.repo_id = 0 AND label.org_id != repository.owner_id))", CommentTypeLabel). + Table("comment"). + Join("inner", "label", "label.id = comment.label_id"). + Join("inner", "issue", "issue.id = comment.issue_id "). + Join("inner", "repository", "issue.repo_id = repository.id"). + Count() +} + +// FixCommentTypeLabelWithOutsideLabels count label comments with outside label +func FixCommentTypeLabelWithOutsideLabels() (int64, error) { + res, err := db.GetEngine(db.DefaultContext).Exec(`DELETE FROM comment WHERE comment.id IN ( + SELECT il_too.id FROM ( + SELECT com.id + FROM comment AS com + INNER JOIN label ON com.label_id = label.id + INNER JOIN issue on issue.id = com.issue_id + INNER JOIN repository ON issue.repo_id = repository.id + WHERE + com.type = ? AND ((label.org_id = 0 AND issue.repo_id != label.repo_id) OR (label.repo_id = 0 AND label.org_id != repository.owner_id)) + ) AS il_too)`, CommentTypeLabel) + if err != nil { + return 0, err + } + + return res.RowsAffected() +} diff --git a/models/issue_comment_list.go b/models/issues/comment_list.go index d62984c1e6..e3406a5cbe 100644 --- a/models/issue_comment_list.go +++ b/models/issues/comment_list.go @@ -2,13 +2,12 @@ // Use of this source code is governed by a MIT-style // license that can be found in the LICENSE file. -package models +package issues import ( "context" "code.gitea.io/gitea/models/db" - issues_model "code.gitea.io/gitea/models/issues" repo_model "code.gitea.io/gitea/models/repo" user_model "code.gitea.io/gitea/models/user" "code.gitea.io/gitea/modules/container" @@ -36,7 +35,7 @@ func (comments CommentList) loadPosters(ctx context.Context) error { posterMaps := make(map[int64]*user_model.User, len(posterIDs)) left := len(posterIDs) for left > 0 { - limit := defaultMaxInSize + limit := db.DefaultMaxInSize if left < limit { limit = left } @@ -80,7 +79,7 @@ func (comments CommentList) getLabelIDs() []int64 { return container.KeysInt64(ids) } -func (comments CommentList) loadLabels(ctx context.Context) error { +func (comments CommentList) loadLabels(ctx context.Context) error { //nolint if len(comments) == 0 { return nil } @@ -89,7 +88,7 @@ func (comments CommentList) loadLabels(ctx context.Context) error { commentLabels := make(map[int64]*Label, len(labelIDs)) left := len(labelIDs) for left > 0 { - limit := defaultMaxInSize + limit := db.DefaultMaxInSize if left < limit { limit = left } @@ -140,10 +139,10 @@ func (comments CommentList) loadMilestones(ctx context.Context) error { return nil } - milestoneMaps := make(map[int64]*issues_model.Milestone, len(milestoneIDs)) + milestoneMaps := make(map[int64]*Milestone, len(milestoneIDs)) left := len(milestoneIDs) for left > 0 { - limit := defaultMaxInSize + limit := db.DefaultMaxInSize if left < limit { limit = left } @@ -183,10 +182,10 @@ func (comments CommentList) loadOldMilestones(ctx context.Context) error { return nil } - milestoneMaps := make(map[int64]*issues_model.Milestone, len(milestoneIDs)) + milestoneMaps := make(map[int64]*Milestone, len(milestoneIDs)) left := len(milestoneIDs) for left > 0 { - limit := defaultMaxInSize + limit := db.DefaultMaxInSize if left < limit { limit = left } @@ -225,7 +224,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 := defaultMaxInSize + limit := db.DefaultMaxInSize if left < limit { limit = left } @@ -299,7 +298,7 @@ func (comments CommentList) loadIssues(ctx context.Context) error { issues := make(map[int64]*Issue, len(issueIDs)) left := len(issueIDs) for left > 0 { - limit := defaultMaxInSize + limit := db.DefaultMaxInSize if left < limit { limit = left } @@ -357,7 +356,7 @@ func (comments CommentList) loadDependentIssues(ctx context.Context) error { issues := make(map[int64]*Issue, len(issueIDs)) left := len(issueIDs) for left > 0 { - limit := defaultMaxInSize + limit := db.DefaultMaxInSize if left < limit { limit = left } @@ -406,7 +405,7 @@ func (comments CommentList) loadAttachments(ctx context.Context) (err error) { commentsIDs := comments.getCommentIDs() left := len(commentsIDs) for left > 0 { - limit := defaultMaxInSize + limit := db.DefaultMaxInSize if left < limit { limit = left } @@ -449,7 +448,7 @@ func (comments CommentList) getReviewIDs() []int64 { return container.KeysInt64(ids) } -func (comments CommentList) loadReviews(ctx context.Context) error { +func (comments CommentList) loadReviews(ctx context.Context) error { //nolint if len(comments) == 0 { return nil } @@ -458,7 +457,7 @@ func (comments CommentList) loadReviews(ctx context.Context) error { reviews := make(map[int64]*Review, len(reviewIDs)) left := len(reviewIDs) for left > 0 { - limit := defaultMaxInSize + limit := db.DefaultMaxInSize if left < limit { limit = left } diff --git a/models/issue_comment_test.go b/models/issues/comment_test.go index d323a08167..06b0b85e3c 100644 --- a/models/issue_comment_test.go +++ b/models/issues/comment_test.go @@ -2,13 +2,14 @@ // Use of this source code is governed by a MIT-style // license that can be found in the LICENSE file. -package models +package issues_test import ( "testing" "time" "code.gitea.io/gitea/models/db" + issues_model "code.gitea.io/gitea/models/issues" repo_model "code.gitea.io/gitea/models/repo" "code.gitea.io/gitea/models/unittest" user_model "code.gitea.io/gitea/models/user" @@ -19,13 +20,13 @@ import ( func TestCreateComment(t *testing.T) { assert.NoError(t, unittest.PrepareTestDatabase()) - issue := unittest.AssertExistsAndLoadBean(t, &Issue{}).(*Issue) + issue := unittest.AssertExistsAndLoadBean(t, &issues_model.Issue{}).(*issues_model.Issue) repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: issue.RepoID}).(*repo_model.Repository) doer := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: repo.OwnerID}).(*user_model.User) now := time.Now().Unix() - comment, err := CreateComment(&CreateCommentOptions{ - Type: CommentTypeComment, + comment, err := issues_model.CreateComment(&issues_model.CreateCommentOptions{ + Type: issues_model.CommentTypeComment, Doer: doer, Repo: repo, Issue: issue, @@ -34,23 +35,23 @@ func TestCreateComment(t *testing.T) { assert.NoError(t, err) then := time.Now().Unix() - assert.EqualValues(t, CommentTypeComment, comment.Type) + 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) unittest.AssertInt64InRange(t, now, then, int64(comment.CreatedUnix)) unittest.AssertExistsAndLoadBean(t, comment) // assert actually added to DB - updatedIssue := unittest.AssertExistsAndLoadBean(t, &Issue{ID: issue.ID}).(*Issue) + updatedIssue := unittest.AssertExistsAndLoadBean(t, &issues_model.Issue{ID: issue.ID}).(*issues_model.Issue) unittest.AssertInt64InRange(t, now, then, int64(updatedIssue.UpdatedUnix)) } func TestFetchCodeComments(t *testing.T) { assert.NoError(t, unittest.PrepareTestDatabase()) - issue := unittest.AssertExistsAndLoadBean(t, &Issue{ID: 2}).(*Issue) + issue := unittest.AssertExistsAndLoadBean(t, &issues_model.Issue{ID: 2}).(*issues_model.Issue) user := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 1}).(*user_model.User) - res, err := FetchCodeComments(db.DefaultContext, issue, user) + res, err := issues_model.FetchCodeComments(db.DefaultContext, issue, user) assert.NoError(t, err) assert.Contains(t, res, "README.md") assert.Contains(t, res["README.md"], int64(4)) @@ -58,7 +59,7 @@ func TestFetchCodeComments(t *testing.T) { assert.Equal(t, int64(4), res["README.md"][4][0].ID) user2 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2}).(*user_model.User) - res, err = FetchCodeComments(db.DefaultContext, issue, user2) + res, err = issues_model.FetchCodeComments(db.DefaultContext, issue, user2) assert.NoError(t, err) assert.Len(t, res, 1) } diff --git a/models/issues/content_history.go b/models/issues/content_history.go index 4c5af13db7..3e321784bd 100644 --- a/models/issues/content_history.go +++ b/models/issues/content_history.go @@ -53,13 +53,13 @@ func SaveIssueContentHistory(ctx context.Context, posterID, issueID, commentID i } // We only keep at most 20 history revisions now. It is enough in most cases. // If there is a special requirement to keep more, we can consider introducing a new setting option then, but not now. - keepLimitedContentHistory(ctx, issueID, commentID, 20) + KeepLimitedContentHistory(ctx, issueID, commentID, 20) return nil } -// keepLimitedContentHistory keeps at most `limit` history revisions, it will hard delete out-dated revisions, sorting by revision interval +// KeepLimitedContentHistory keeps at most `limit` history revisions, it will hard delete out-dated revisions, sorting by revision interval // we can ignore all errors in this function, so we just log them -func keepLimitedContentHistory(ctx context.Context, issueID, commentID int64, limit int) { +func KeepLimitedContentHistory(ctx context.Context, issueID, commentID int64, limit int) { type IDEditTime struct { ID int64 EditedUnix timeutil.TimeStamp diff --git a/models/issues/content_history_test.go b/models/issues/content_history_test.go index 3cbc0ad5e0..1218d871d0 100644 --- a/models/issues/content_history_test.go +++ b/models/issues/content_history_test.go @@ -2,12 +2,13 @@ // Use of this source code is governed by a MIT-style // license that can be found in the LICENSE file. -package issues +package issues_test import ( "testing" "code.gitea.io/gitea/models/db" + issues_model "code.gitea.io/gitea/models/issues" "code.gitea.io/gitea/models/unittest" "code.gitea.io/gitea/modules/timeutil" @@ -20,20 +21,20 @@ func TestContentHistory(t *testing.T) { dbCtx := db.DefaultContext timeStampNow := timeutil.TimeStampNow() - _ = SaveIssueContentHistory(dbCtx, 1, 10, 0, timeStampNow, "i-a", true) - _ = SaveIssueContentHistory(dbCtx, 1, 10, 0, timeStampNow.Add(2), "i-b", false) - _ = SaveIssueContentHistory(dbCtx, 1, 10, 0, timeStampNow.Add(7), "i-c", false) + _ = issues_model.SaveIssueContentHistory(dbCtx, 1, 10, 0, timeStampNow, "i-a", true) + _ = issues_model.SaveIssueContentHistory(dbCtx, 1, 10, 0, timeStampNow.Add(2), "i-b", false) + _ = issues_model.SaveIssueContentHistory(dbCtx, 1, 10, 0, timeStampNow.Add(7), "i-c", false) - _ = SaveIssueContentHistory(dbCtx, 1, 10, 100, timeStampNow, "c-a", true) - _ = SaveIssueContentHistory(dbCtx, 1, 10, 100, timeStampNow.Add(5), "c-b", false) - _ = SaveIssueContentHistory(dbCtx, 1, 10, 100, timeStampNow.Add(20), "c-c", false) - _ = SaveIssueContentHistory(dbCtx, 1, 10, 100, timeStampNow.Add(50), "c-d", false) - _ = SaveIssueContentHistory(dbCtx, 1, 10, 100, timeStampNow.Add(51), "c-e", false) + _ = issues_model.SaveIssueContentHistory(dbCtx, 1, 10, 100, timeStampNow, "c-a", true) + _ = issues_model.SaveIssueContentHistory(dbCtx, 1, 10, 100, timeStampNow.Add(5), "c-b", false) + _ = issues_model.SaveIssueContentHistory(dbCtx, 1, 10, 100, timeStampNow.Add(20), "c-c", false) + _ = issues_model.SaveIssueContentHistory(dbCtx, 1, 10, 100, timeStampNow.Add(50), "c-d", false) + _ = issues_model.SaveIssueContentHistory(dbCtx, 1, 10, 100, timeStampNow.Add(51), "c-e", false) - h1, _ := GetIssueContentHistoryByID(dbCtx, 1) + h1, _ := issues_model.GetIssueContentHistoryByID(dbCtx, 1) assert.EqualValues(t, 1, h1.ID) - m, _ := QueryIssueContentHistoryEditedCountMap(dbCtx, 10) + m, _ := issues_model.QueryIssueContentHistoryEditedCountMap(dbCtx, 10) assert.Equal(t, 3, m[0]) assert.Equal(t, 5, m[100]) @@ -48,31 +49,31 @@ func TestContentHistory(t *testing.T) { } _ = db.GetEngine(dbCtx).Sync2(&User{}) - list1, _ := FetchIssueContentHistoryList(dbCtx, 10, 0) + list1, _ := issues_model.FetchIssueContentHistoryList(dbCtx, 10, 0) assert.Len(t, list1, 3) - list2, _ := FetchIssueContentHistoryList(dbCtx, 10, 100) + list2, _ := issues_model.FetchIssueContentHistoryList(dbCtx, 10, 100) assert.Len(t, list2, 5) - hasHistory1, _ := HasIssueContentHistory(dbCtx, 10, 0) + hasHistory1, _ := issues_model.HasIssueContentHistory(dbCtx, 10, 0) assert.True(t, hasHistory1) - hasHistory2, _ := HasIssueContentHistory(dbCtx, 10, 1) + hasHistory2, _ := issues_model.HasIssueContentHistory(dbCtx, 10, 1) assert.False(t, hasHistory2) - h6, h6Prev, _ := GetIssueContentHistoryAndPrev(dbCtx, 6) + h6, h6Prev, _ := issues_model.GetIssueContentHistoryAndPrev(dbCtx, 6) assert.EqualValues(t, 6, h6.ID) assert.EqualValues(t, 5, h6Prev.ID) // soft-delete - _ = SoftDeleteIssueContentHistory(dbCtx, 5) - h6, h6Prev, _ = GetIssueContentHistoryAndPrev(dbCtx, 6) + _ = issues_model.SoftDeleteIssueContentHistory(dbCtx, 5) + h6, h6Prev, _ = issues_model.GetIssueContentHistoryAndPrev(dbCtx, 6) assert.EqualValues(t, 6, h6.ID) assert.EqualValues(t, 4, h6Prev.ID) // only keep 3 history revisions for comment_id=100, the first and the last should never be deleted - keepLimitedContentHistory(dbCtx, 10, 100, 3) - list1, _ = FetchIssueContentHistoryList(dbCtx, 10, 0) + issues_model.KeepLimitedContentHistory(dbCtx, 10, 100, 3) + list1, _ = issues_model.FetchIssueContentHistoryList(dbCtx, 10, 0) assert.Len(t, list1, 3) - list2, _ = FetchIssueContentHistoryList(dbCtx, 10, 100) + list2, _ = issues_model.FetchIssueContentHistoryList(dbCtx, 10, 100) assert.Len(t, list2, 3) assert.EqualValues(t, 8, list2[0].HistoryID) assert.EqualValues(t, 7, list2[1].HistoryID) diff --git a/models/issue_dependency.go b/models/issues/dependency.go index af40aa45d3..d664c0758e 100644 --- a/models/issue_dependency.go +++ b/models/issues/dependency.go @@ -2,16 +2,95 @@ // Use of this source code is governed by a MIT-style // license that can be found in the LICENSE file. -package models +package issues import ( "context" + "fmt" "code.gitea.io/gitea/models/db" user_model "code.gitea.io/gitea/models/user" "code.gitea.io/gitea/modules/timeutil" ) +// ErrDependencyExists represents a "DependencyAlreadyExists" kind of error. +type ErrDependencyExists struct { + IssueID int64 + DependencyID int64 +} + +// IsErrDependencyExists checks if an error is a ErrDependencyExists. +func IsErrDependencyExists(err error) bool { + _, ok := err.(ErrDependencyExists) + return ok +} + +func (err ErrDependencyExists) Error() string { + return fmt.Sprintf("issue dependency does already exist [issue id: %d, dependency id: %d]", err.IssueID, err.DependencyID) +} + +// ErrDependencyNotExists represents a "DependencyAlreadyExists" kind of error. +type ErrDependencyNotExists struct { + IssueID int64 + DependencyID int64 +} + +// IsErrDependencyNotExists checks if an error is a ErrDependencyExists. +func IsErrDependencyNotExists(err error) bool { + _, ok := err.(ErrDependencyNotExists) + return ok +} + +func (err ErrDependencyNotExists) Error() string { + return fmt.Sprintf("issue dependency does not exist [issue id: %d, dependency id: %d]", err.IssueID, err.DependencyID) +} + +// ErrCircularDependency represents a "DependencyCircular" kind of error. +type ErrCircularDependency struct { + IssueID int64 + DependencyID int64 +} + +// IsErrCircularDependency checks if an error is a ErrCircularDependency. +func IsErrCircularDependency(err error) bool { + _, ok := err.(ErrCircularDependency) + return ok +} + +func (err ErrCircularDependency) Error() string { + return fmt.Sprintf("circular dependencies exists (two issues blocking each other) [issue id: %d, dependency id: %d]", err.IssueID, err.DependencyID) +} + +// ErrDependenciesLeft represents an error where the issue you're trying to close still has dependencies left. +type ErrDependenciesLeft struct { + IssueID int64 +} + +// IsErrDependenciesLeft checks if an error is a ErrDependenciesLeft. +func IsErrDependenciesLeft(err error) bool { + _, ok := err.(ErrDependenciesLeft) + return ok +} + +func (err ErrDependenciesLeft) Error() string { + return fmt.Sprintf("issue has open dependencies [issue id: %d]", err.IssueID) +} + +// ErrUnknownDependencyType represents an error where an unknown dependency type was passed +type ErrUnknownDependencyType struct { + Type DependencyType +} + +// IsErrUnknownDependencyType checks if an error is ErrUnknownDependencyType +func IsErrUnknownDependencyType(err error) bool { + _, ok := err.(ErrUnknownDependencyType) + return ok +} + +func (err ErrUnknownDependencyType) Error() string { + return fmt.Sprintf("unknown dependency type [type: %d]", err.Type) +} + // IssueDependency represents an issue dependency type IssueDependency struct { ID int64 `xorm:"pk autoincr"` diff --git a/models/issue_dependency_test.go b/models/issues/dependency_test.go index 345a9077cd..3ea0b4ff5c 100644 --- a/models/issue_dependency_test.go +++ b/models/issues/dependency_test.go @@ -2,12 +2,13 @@ // Use of this source code is governed by a MIT-style // license that can be found in the LICENSE file. -package models +package issues_test import ( "testing" "code.gitea.io/gitea/models/db" + issues_model "code.gitea.io/gitea/models/issues" "code.gitea.io/gitea/models/unittest" user_model "code.gitea.io/gitea/models/user" @@ -21,42 +22,42 @@ func TestCreateIssueDependency(t *testing.T) { user1, err := user_model.GetUserByID(1) assert.NoError(t, err) - issue1, err := GetIssueByID(1) + issue1, err := issues_model.GetIssueByID(db.DefaultContext, 1) assert.NoError(t, err) - issue2, err := GetIssueByID(2) + issue2, err := issues_model.GetIssueByID(db.DefaultContext, 2) assert.NoError(t, err) // Create a dependency and check if it was successful - err = CreateIssueDependency(user1, issue1, issue2) + err = issues_model.CreateIssueDependency(user1, issue1, issue2) assert.NoError(t, err) // Do it again to see if it will check if the dependency already exists - err = CreateIssueDependency(user1, issue1, issue2) + err = issues_model.CreateIssueDependency(user1, issue1, issue2) assert.Error(t, err) - assert.True(t, IsErrDependencyExists(err)) + assert.True(t, issues_model.IsErrDependencyExists(err)) // Check for circular dependencies - err = CreateIssueDependency(user1, issue2, issue1) + err = issues_model.CreateIssueDependency(user1, issue2, issue1) assert.Error(t, err) - assert.True(t, IsErrCircularDependency(err)) + assert.True(t, issues_model.IsErrCircularDependency(err)) - _ = unittest.AssertExistsAndLoadBean(t, &Comment{Type: CommentTypeAddDependency, PosterID: user1.ID, IssueID: issue1.ID}) + _ = unittest.AssertExistsAndLoadBean(t, &issues_model.Comment{Type: issues_model.CommentTypeAddDependency, PosterID: user1.ID, IssueID: issue1.ID}) // Check if dependencies left is correct - left, err := IssueNoDependenciesLeft(db.DefaultContext, issue1) + left, err := issues_model.IssueNoDependenciesLeft(db.DefaultContext, issue1) assert.NoError(t, err) assert.False(t, left) // Close #2 and check again - _, err = ChangeIssueStatus(db.DefaultContext, issue2, user1, true) + _, err = issues_model.ChangeIssueStatus(db.DefaultContext, issue2, user1, true) assert.NoError(t, err) - left, err = IssueNoDependenciesLeft(db.DefaultContext, issue1) + left, err = issues_model.IssueNoDependenciesLeft(db.DefaultContext, issue1) assert.NoError(t, err) assert.True(t, left) // Test removing the dependency - err = RemoveIssueDependency(user1, issue1, issue2, DependencyTypeBlockedBy) + err = issues_model.RemoveIssueDependency(user1, issue1, issue2, issues_model.DependencyTypeBlockedBy) assert.NoError(t, err) } diff --git a/models/issue.go b/models/issues/issue.go index a22c115523..0f4af3e84f 100644 --- a/models/issue.go +++ b/models/issues/issue.go @@ -3,7 +3,7 @@ // Use of this source code is governed by a MIT-style // license that can be found in the LICENSE file. -package models +package issues import ( "context" @@ -16,7 +16,6 @@ import ( admin_model "code.gitea.io/gitea/models/admin" "code.gitea.io/gitea/models/db" "code.gitea.io/gitea/models/foreignreference" - issues_model "code.gitea.io/gitea/models/issues" "code.gitea.io/gitea/models/organization" "code.gitea.io/gitea/models/perm" access_model "code.gitea.io/gitea/models/perm/access" @@ -29,7 +28,6 @@ import ( "code.gitea.io/gitea/modules/log" "code.gitea.io/gitea/modules/references" "code.gitea.io/gitea/modules/setting" - "code.gitea.io/gitea/modules/storage" api "code.gitea.io/gitea/modules/structs" "code.gitea.io/gitea/modules/timeutil" "code.gitea.io/gitea/modules/util" @@ -38,6 +36,71 @@ import ( "xorm.io/xorm" ) +// ErrIssueNotExist represents a "IssueNotExist" kind of error. +type ErrIssueNotExist struct { + ID int64 + RepoID int64 + Index int64 +} + +// IsErrIssueNotExist checks if an error is a ErrIssueNotExist. +func IsErrIssueNotExist(err error) bool { + _, ok := err.(ErrIssueNotExist) + return ok +} + +func (err ErrIssueNotExist) Error() string { + return fmt.Sprintf("issue does not exist [id: %d, repo_id: %d, index: %d]", err.ID, err.RepoID, err.Index) +} + +// ErrIssueIsClosed represents a "IssueIsClosed" kind of error. +type ErrIssueIsClosed struct { + ID int64 + RepoID int64 + Index int64 +} + +// IsErrIssueIsClosed checks if an error is a ErrIssueNotExist. +func IsErrIssueIsClosed(err error) bool { + _, ok := err.(ErrIssueIsClosed) + return ok +} + +func (err ErrIssueIsClosed) Error() string { + return fmt.Sprintf("issue is closed [id: %d, repo_id: %d, index: %d]", err.ID, err.RepoID, err.Index) +} + +// ErrNewIssueInsert is used when the INSERT statement in newIssue fails +type ErrNewIssueInsert struct { + OriginalError error +} + +// IsErrNewIssueInsert checks if an error is a ErrNewIssueInsert. +func IsErrNewIssueInsert(err error) bool { + _, ok := err.(ErrNewIssueInsert) + return ok +} + +func (err ErrNewIssueInsert) Error() string { + return err.OriginalError.Error() +} + +// ErrIssueWasClosed is used when close a closed issue +type ErrIssueWasClosed struct { + ID int64 + Index int64 +} + +// IsErrIssueWasClosed checks if an error is a ErrIssueWasClosed. +func IsErrIssueWasClosed(err error) bool { + _, ok := err.(ErrIssueWasClosed) + return ok +} + +func (err ErrIssueWasClosed) Error() string { + return fmt.Sprintf("Issue [%d] %d was already closed", err.ID, err.Index) +} + // Issue represents an issue or pull request of repository. type Issue struct { ID int64 `xorm:"pk autoincr"` @@ -47,14 +110,14 @@ type Issue struct { PosterID int64 `xorm:"INDEX"` Poster *user_model.User `xorm:"-"` OriginalAuthor string - OriginalAuthorID int64 `xorm:"index"` - Title string `xorm:"name"` - Content string `xorm:"LONGTEXT"` - RenderedContent string `xorm:"-"` - Labels []*Label `xorm:"-"` - MilestoneID int64 `xorm:"INDEX"` - Milestone *issues_model.Milestone `xorm:"-"` - Project *project_model.Project `xorm:"-"` + OriginalAuthorID int64 `xorm:"index"` + Title string `xorm:"name"` + Content string `xorm:"LONGTEXT"` + RenderedContent string `xorm:"-"` + Labels []*Label `xorm:"-"` + MilestoneID int64 `xorm:"INDEX"` + Milestone *Milestone `xorm:"-"` + Project *project_model.Project `xorm:"-"` Priority int AssigneeID int64 `xorm:"-"` Assignee *user_model.User `xorm:"-"` @@ -73,7 +136,7 @@ type Issue struct { Attachments []*repo_model.Attachment `xorm:"-"` Comments []*Comment `xorm:"-"` - Reactions issues_model.ReactionList `xorm:"-"` + Reactions ReactionList `xorm:"-"` TotalTrackedTime int64 `xorm:"-"` Assignees []*user_model.User `xorm:"-"` ForeignReference *foreignreference.ForeignReference `xorm:"-"` @@ -107,7 +170,8 @@ func init() { db.RegisterModel(new(IssueIndex)) } -func (issue *Issue) loadTotalTimes(ctx context.Context) (err error) { +// LoadTotalTimes load total tracked time +func (issue *Issue) LoadTotalTimes(ctx context.Context) (err error) { opts := FindTrackedTimesOptions{IssueID: issue.ID} issue.TotalTrackedTime, err = opts.toSession(db.GetEngine(ctx)).SumInt(&TrackedTime{}, "time") if err != nil { @@ -237,7 +301,7 @@ func (issue *Issue) loadReactions(ctx context.Context) (err error) { if issue.Reactions != nil { return nil } - reactions, _, err := issues_model.FindReactions(ctx, issues_model.FindReactionsOptions{ + reactions, _, err := FindReactions(ctx, FindReactionsOptions{ IssueID: issue.ID, }) if err != nil { @@ -247,7 +311,7 @@ func (issue *Issue) loadReactions(ctx context.Context) (err error) { return err } // Load reaction user data - if _, err := issues_model.ReactionList(reactions).LoadUsers(ctx, issue.Repo); err != nil { + if _, err := ReactionList(reactions).LoadUsers(ctx, issue.Repo); err != nil { return err } @@ -292,15 +356,16 @@ func (issue *Issue) loadForeignReference(ctx context.Context) (err error) { func (issue *Issue) loadMilestone(ctx context.Context) (err error) { if (issue.Milestone == nil || issue.Milestone.ID != issue.MilestoneID) && issue.MilestoneID > 0 { - issue.Milestone, err = issues_model.GetMilestoneByRepoID(ctx, issue.RepoID, issue.MilestoneID) - if err != nil && !issues_model.IsErrMilestoneNotExist(err) { + issue.Milestone, err = GetMilestoneByRepoID(ctx, issue.RepoID, issue.MilestoneID) + if err != nil && !IsErrMilestoneNotExist(err) { return fmt.Errorf("getMilestoneByRepoID [repo_id: %d, milestone_id: %d]: %v", issue.RepoID, issue.MilestoneID, err) } } return nil } -func (issue *Issue) loadAttributes(ctx context.Context) (err error) { +// LoadAttributes loads the attribute of this issue. +func (issue *Issue) LoadAttributes(ctx context.Context) (err error) { if err = issue.LoadRepo(ctx); err != nil { return } @@ -345,7 +410,7 @@ func (issue *Issue) loadAttributes(ctx context.Context) (err error) { return err } if issue.isTimetrackerEnabled(ctx) { - if err = issue.loadTotalTimes(ctx); err != nil { + if err = issue.LoadTotalTimes(ctx); err != nil { return err } } @@ -357,11 +422,6 @@ func (issue *Issue) loadAttributes(ctx context.Context) (err error) { return issue.loadReactions(ctx) } -// LoadAttributes loads the attribute of this issue. -func (issue *Issue) LoadAttributes() error { - return issue.loadAttributes(db.DefaultContext) -} - // LoadMilestone load milestone of this issue. func (issue *Issue) LoadMilestone() error { return issue.loadMilestone(db.DefaultContext) @@ -590,15 +650,6 @@ func ReplaceIssueLabels(issue *Issue, labels []*Label, doer *user_model.User) (e return committer.Commit() } -// ReadBy sets issue to be read by given user. -func (issue *Issue) ReadBy(ctx context.Context, userID int64) error { - if err := UpdateIssueUserByRead(userID, issue.ID); err != nil { - return err - } - - return setIssueNotificationStatusReadIfUnread(ctx, userID, issue.ID) -} - // UpdateIssueCols updates cols of issue func UpdateIssueCols(ctx context.Context, issue *Issue, cols ...string) error { if _, err := db.GetEngine(ctx).ID(issue.ID).Cols(cols...).Update(issue); err != nil { @@ -609,7 +660,7 @@ func UpdateIssueCols(ctx context.Context, issue *Issue, cols ...string) error { func changeIssueStatus(ctx context.Context, issue *Issue, doer *user_model.User, isClosed, isMergePull bool) (*Comment, error) { // Reload the issue - currentIssue, err := getIssueByID(ctx, issue.ID) + currentIssue, err := GetIssueByID(ctx, issue.ID) if err != nil { return nil, err } @@ -666,7 +717,7 @@ func doChangeIssueStatus(ctx context.Context, issue *Issue, doer *user_model.Use // Update issue count of milestone if issue.MilestoneID > 0 { - if err := issues_model.UpdateMilestoneCounters(ctx, issue.MilestoneID); err != nil { + if err := UpdateMilestoneCounters(ctx, issue.MilestoneID); err != nil { return nil, err } } @@ -730,7 +781,7 @@ func ChangeIssueTitle(issue *Issue, doer *user_model.User, oldTitle string) (err if _, err = CreateCommentCtx(ctx, opts); err != nil { return fmt.Errorf("createComment: %v", err) } - if err = issue.addCrossReferences(ctx, doer, true); err != nil { + if err = issue.AddCrossReferences(ctx, doer, true); err != nil { return err } @@ -772,7 +823,7 @@ func ChangeIssueRef(issue *Issue, doer *user_model.User, oldRef string) (err err // AddDeletePRBranchComment adds delete branch comment for pull request issue func AddDeletePRBranchComment(ctx context.Context, doer *user_model.User, repo *repo_model.Repository, issueID int64, branchName string) error { - issue, err := getIssueByID(ctx, issueID) + issue, err := GetIssueByID(ctx, issueID) if err != nil { return err } @@ -815,12 +866,12 @@ func ChangeIssueContent(issue *Issue, doer *user_model.User, content string) (er } defer committer.Close() - hasContentHistory, err := issues_model.HasIssueContentHistory(ctx, issue.ID, 0) + hasContentHistory, err := HasIssueContentHistory(ctx, issue.ID, 0) if err != nil { return fmt.Errorf("HasIssueContentHistory: %v", err) } if !hasContentHistory { - if err = issues_model.SaveIssueContentHistory(ctx, issue.PosterID, issue.ID, 0, + if err = SaveIssueContentHistory(ctx, issue.PosterID, issue.ID, 0, issue.CreatedUnix, issue.Content, true); err != nil { return fmt.Errorf("SaveIssueContentHistory: %v", err) } @@ -832,12 +883,12 @@ func ChangeIssueContent(issue *Issue, doer *user_model.User, content string) (er return fmt.Errorf("UpdateIssueCols: %v", err) } - if err = issues_model.SaveIssueContentHistory(ctx, doer.ID, issue.ID, 0, + if err = SaveIssueContentHistory(ctx, doer.ID, issue.ID, 0, timeutil.TimeStampNow(), issue.Content, false); err != nil { return fmt.Errorf("SaveIssueContentHistory: %v", err) } - if err = issue.addCrossReferences(ctx, doer, true); err != nil { + if err = issue.AddCrossReferences(ctx, doer, true); err != nil { return fmt.Errorf("addCrossReferences: %v", err) } @@ -907,13 +958,14 @@ type NewIssueOptions struct { IsPull bool } -func newIssue(ctx context.Context, doer *user_model.User, opts NewIssueOptions) (err error) { +// NewIssueWithIndex creates issue with given index +func NewIssueWithIndex(ctx context.Context, doer *user_model.User, opts NewIssueOptions) (err error) { e := db.GetEngine(ctx) opts.Issue.Title = strings.TrimSpace(opts.Issue.Title) if opts.Issue.MilestoneID > 0 { - milestone, err := issues_model.GetMilestoneByRepoID(ctx, opts.Issue.RepoID, opts.Issue.MilestoneID) - if err != nil && !issues_model.IsErrMilestoneNotExist(err) { + milestone, err := GetMilestoneByRepoID(ctx, opts.Issue.RepoID, opts.Issue.MilestoneID) + if err != nil && !IsErrMilestoneNotExist(err) { return fmt.Errorf("getMilestoneByID: %v", err) } @@ -937,7 +989,7 @@ func newIssue(ctx context.Context, doer *user_model.User, opts NewIssueOptions) } if opts.Issue.MilestoneID > 0 { - if err := issues_model.UpdateMilestoneCounters(ctx, opts.Issue.MilestoneID); err != nil { + if err := UpdateMilestoneCounters(ctx, opts.Issue.MilestoneID); err != nil { return err } @@ -987,7 +1039,7 @@ func newIssue(ctx context.Context, doer *user_model.User, opts NewIssueOptions) } } - if err = newIssueUsers(ctx, opts.Repo, opts.Issue); err != nil { + if err = NewIssueUsers(ctx, opts.Repo, opts.Issue); err != nil { return err } @@ -1004,36 +1056,11 @@ func newIssue(ctx context.Context, doer *user_model.User, opts NewIssueOptions) } } } - if err = opts.Issue.loadAttributes(ctx); err != nil { + if err = opts.Issue.LoadAttributes(ctx); err != nil { return err } - return opts.Issue.addCrossReferences(ctx, doer, false) -} - -// RecalculateIssueIndexForRepo create issue_index for repo if not exist and -// update it based on highest index of existing issues assigned to a repo -func RecalculateIssueIndexForRepo(repoID int64) error { - ctx, committer, err := db.TxContext() - if err != nil { - return err - } - defer committer.Close() - - if err := db.UpsertResourceIndex(db.GetEngine(ctx), "issue_index", repoID); err != nil { - return err - } - - var max int64 - if _, err := db.GetEngine(ctx).Select(" MAX(`index`)").Table("issue").Where("repo_id=?", repoID).Get(&max); err != nil { - return err - } - - if _, err := db.GetEngine(ctx).Exec("UPDATE `issue_index` SET max_index=? WHERE group_id=?", max, repoID); err != nil { - return err - } - - return committer.Commit() + return opts.Issue.AddCrossReferences(ctx, doer, false) } // NewIssue creates new issue with labels for repository. @@ -1051,13 +1078,13 @@ func NewIssue(repo *repo_model.Repository, issue *Issue, labelIDs []int64, uuids } defer committer.Close() - if err = newIssue(ctx, issue.Poster, NewIssueOptions{ + if err = NewIssueWithIndex(ctx, issue.Poster, NewIssueOptions{ Repo: repo, Issue: issue, LabelIDs: labelIDs, Attachments: uuids, }); err != nil { - if IsErrUserDoesNotHaveAccessToRepo(err) || IsErrNewIssueInsert(err) { + if repo_model.IsErrUserDoesNotHaveAccessToRepo(err) || IsErrNewIssueInsert(err) { return err } return fmt.Errorf("newIssue: %v", err) @@ -1114,10 +1141,11 @@ func GetIssueWithAttrsByIndex(repoID, index int64) (*Issue, error) { if err != nil { return nil, err } - return issue, issue.LoadAttributes() + return issue, issue.LoadAttributes(db.DefaultContext) } -func getIssueByID(ctx context.Context, id int64) (*Issue, error) { +// GetIssueByID returns an issue by given ID. +func GetIssueByID(ctx context.Context, id int64) (*Issue, error) { issue := new(Issue) has, err := db.GetEngine(ctx).ID(id).Get(issue) if err != nil { @@ -1130,16 +1158,11 @@ func getIssueByID(ctx context.Context, id int64) (*Issue, error) { // GetIssueWithAttrsByID returns an issue with attributes by given ID. func GetIssueWithAttrsByID(id int64) (*Issue, error) { - issue, err := getIssueByID(db.DefaultContext, id) + issue, err := GetIssueByID(db.DefaultContext, id) if err != nil { return nil, err } - return issue, issue.loadAttributes(db.DefaultContext) -} - -// GetIssueByID returns an issue by given ID. -func GetIssueByID(id int64) (*Issue, error) { - return getIssueByID(db.DefaultContext, id) + return issue, issue.LoadAttributes(db.DefaultContext) } // GetIssuesByIDs return issues with the given IDs. @@ -1156,7 +1179,7 @@ func GetIssueIDsByRepoID(ctx context.Context, repoID int64) ([]int64, error) { } // IssuesOptions represents options of an issue. -type IssuesOptions struct { +type IssuesOptions struct { //nolint db.ListOptions RepoID int64 // overwrites RepoCond if not 0 RepoCond builder.Cond @@ -1534,7 +1557,7 @@ func GetParticipantsIDsByIssueID(issueID int64) ([]int64, error) { // IsUserParticipantsOfIssue return true if user is participants of an issue func IsUserParticipantsOfIssue(user *user_model.User, issue *Issue) bool { - userIDs, err := issue.getParticipantIDsByIssue(db.DefaultContext) + userIDs, err := issue.GetParticipantIDsByIssue(db.DefaultContext) if err != nil { log.Error(err.Error()) return false @@ -1577,17 +1600,6 @@ const ( FilterModeYourRepositories ) -func parseCountResult(results []map[string][]byte) int64 { - if len(results) == 0 { - return 0 - } - for _, result := range results[0] { - c, _ := strconv.ParseInt(string(result), 10, 64) - return c - } - return 0 -} - // IssueStatsOptions contains parameters accepted by GetIssueStats. type IssueStatsOptions struct { RepoID int64 @@ -1602,14 +1614,15 @@ type IssueStatsOptions struct { } const ( + // MaxQueryParameters represents the max query parameters // When queries are broken down in parts because of the number // of parameters, attempt to break by this amount - maxQueryParameters = 300 + MaxQueryParameters = 300 ) // GetIssueStats returns issue statistic information by given conditions. func GetIssueStats(opts *IssueStatsOptions) (*IssueStats, error) { - if len(opts.IssueIDs) <= maxQueryParameters { + if len(opts.IssueIDs) <= MaxQueryParameters { return getIssueStatsChunk(opts, opts.IssueIDs) } @@ -1619,7 +1632,7 @@ func GetIssueStats(opts *IssueStatsOptions) (*IssueStats, error) { // ids in a temporary table and join from them. accum := &IssueStats{} for i := 0; i < len(opts.IssueIDs); { - chunk := i + maxQueryParameters + chunk := i + MaxQueryParameters if chunk > len(opts.IssueIDs) { chunk = len(opts.IssueIDs) } @@ -1950,7 +1963,7 @@ func UpdateIssueByAPI(issue *Issue, doer *user_model.User) (statusChangeComment } // Reload the issue - currentIssue, err := getIssueByID(ctx, issue.ID) + currentIssue, err := GetIssueByID(ctx, issue.ID) if err != nil { return nil, false, err } @@ -1985,7 +1998,7 @@ func UpdateIssueByAPI(issue *Issue, doer *user_model.User) (statusChangeComment } } - if err := issue.addCrossReferences(ctx, doer, true); err != nil { + if err := issue.AddCrossReferences(ctx, doer, true); err != nil { return nil, false, err } return statusChangeComment, titleChanged, committer.Commit() @@ -2016,22 +2029,8 @@ func UpdateIssueDeadline(issue *Issue, deadlineUnix timeutil.TimeStamp, doer *us return committer.Commit() } -// DeleteIssue deletes the issue -func DeleteIssue(issue *Issue) error { - ctx, committer, err := db.TxContext() - if err != nil { - return err - } - defer committer.Close() - - if err := deleteIssue(ctx, issue); err != nil { - return err - } - - return committer.Commit() -} - -func deleteInIssue(ctx context.Context, issueID int64, beans ...interface{}) error { +// DeleteInIssue delete records in beans with external key issue_id = ? +func DeleteInIssue(ctx context.Context, issueID int64, beans ...interface{}) error { e := db.GetEngine(ctx) for _, bean := range beans { if _, err := e.In("issue_id", issueID).Delete(bean); err != nil { @@ -2041,103 +2040,14 @@ func deleteInIssue(ctx context.Context, issueID int64, beans ...interface{}) err return nil } -func deleteIssue(ctx context.Context, issue *Issue) error { - e := db.GetEngine(ctx) - if _, err := e.ID(issue.ID).NoAutoCondition().Delete(issue); err != nil { - return err - } - - if issue.IsPull { - if _, err := e.ID(issue.RepoID).Decr("num_pulls").Update(new(repo_model.Repository)); err != nil { - return err - } - if issue.IsClosed { - if _, err := e.ID(issue.RepoID).Decr("num_closed_pulls").Update(new(repo_model.Repository)); err != nil { - return err - } - } - } else { - if _, err := e.ID(issue.RepoID).Decr("num_issues").Update(new(repo_model.Repository)); err != nil { - return err - } - if issue.IsClosed { - if _, err := e.ID(issue.RepoID).Decr("num_closed_issues").Update(new(repo_model.Repository)); err != nil { - return err - } - } - } - - // delete actions assigned to this issue - subQuery := builder.Select("`id`"). - From("`comment`"). - Where(builder.Eq{"`issue_id`": issue.ID}) - if _, err := e.In("comment_id", subQuery).Delete(&Action{}); err != nil { - return err - } - - if _, err := e.Table("action").Where("repo_id = ?", issue.RepoID). - In("op_type", ActionCreateIssue, ActionCreatePullRequest). - Where("content LIKE ?", strconv.FormatInt(issue.ID, 10)+"|%"). - Delete(&Action{}); err != nil { - return err - } - - // find attachments related to this issue and remove them - var attachments []*repo_model.Attachment - if err := e.In("issue_id", issue.ID).Find(&attachments); err != nil { - return err - } - - for i := range attachments { - admin_model.RemoveStorageWithNotice(ctx, storage.Attachments, "Delete issue attachment", attachments[i].RelativePath()) - } - - // delete all database data still assigned to this issue - if err := deleteInIssue(ctx, issue.ID, - &issues_model.ContentHistory{}, - &Comment{}, - &IssueLabel{}, - &IssueDependency{}, - &IssueAssignees{}, - &IssueUser{}, - &Notification{}, - &issues_model.Reaction{}, - &IssueWatch{}, - &Stopwatch{}, - &TrackedTime{}, - &project_model.ProjectIssue{}, - &repo_model.Attachment{}, - &PullRequest{}, - ); err != nil { - return err - } - - // References to this issue in other issues - if _, err := e.In("ref_issue_id", issue.ID).Delete(&Comment{}); err != nil { - return err - } - - // Delete dependencies for issues in other repositories - if _, err := e.In("dependency_id", issue.ID).Delete(&IssueDependency{}); err != nil { - return err - } - - // delete from dependent issues - if _, err := e.In("dependent_issue_id", issue.ID).Delete(&Comment{}); err != nil { - return err - } - - return nil -} - // DependencyInfo represents high level information about an issue which is a dependency of another issue. type DependencyInfo struct { Issue `xorm:"extends"` repo_model.Repository `xorm:"extends"` } -// getParticipantIDsByIssue returns all userIDs who are participated in comments of an issue and issue author -func (issue *Issue) getParticipantIDsByIssue(ctx context.Context) ([]int64, error) { +// GetParticipantIDsByIssue returns all userIDs who are participated in comments of an issue and issue author +func (issue *Issue) GetParticipantIDsByIssue(ctx context.Context) ([]int64, error) { if issue == nil { return nil, nil } @@ -2196,9 +2106,9 @@ func (issue *Issue) BlockingDependencies(ctx context.Context) (issueDeps []*Depe func updateIssueClosedNum(ctx context.Context, issue *Issue) (err error) { if issue.IsPull { - err = repoStatsCorrectNumClosed(ctx, issue.RepoID, true, "num_closed_pulls") + err = repo_model.StatsCorrectNumClosed(ctx, issue.RepoID, true, "num_closed_pulls") } else { - err = repoStatsCorrectNumClosed(ctx, issue.RepoID, false, "num_closed_issues") + err = repo_model.StatsCorrectNumClosed(ctx, issue.RepoID, false, "num_closed_issues") } return } @@ -2363,6 +2273,17 @@ func UpdateIssuesMigrationsByType(gitServiceType api.GitServiceType, originalAut return err } +func migratedIssueCond(tp api.GitServiceType) builder.Cond { + return builder.In("issue_id", + builder.Select("issue.id"). + From("issue"). + InnerJoin("repository", "issue.repo_id = repository.id"). + Where(builder.Eq{ + "repository.original_service_type": tp, + }), + ) +} + // UpdateReactionsMigrationsByType updates all migrated repositories' reactions from gitServiceType to replace originalAuthorID to posterID func UpdateReactionsMigrationsByType(gitServiceType api.GitServiceType, originalAuthorID string, userID int64) error { _, err := db.GetEngine(db.DefaultContext).Table("reaction"). @@ -2376,13 +2297,14 @@ func UpdateReactionsMigrationsByType(gitServiceType api.GitServiceType, original return err } -func deleteIssuesByRepoID(ctx context.Context, repoID int64) (attachmentPaths []string, err error) { +// DeleteIssuesByRepoID deletes issues by repositories id +func DeleteIssuesByRepoID(ctx context.Context, repoID int64) (attachmentPaths []string, err error) { deleteCond := builder.Select("id").From("issue").Where(builder.Eq{"issue.repo_id": repoID}) sess := db.GetEngine(ctx) // Delete content histories if _, err = sess.In("issue_id", deleteCond). - Delete(&issues_model.ContentHistory{}); err != nil { + Delete(&ContentHistory{}); err != nil { return } @@ -2410,7 +2332,7 @@ func deleteIssuesByRepoID(ctx context.Context, repoID int64) (attachmentPaths [] } if _, err = sess.In("issue_id", deleteCond). - Delete(&issues_model.Reaction{}); err != nil { + Delete(&Reaction{}); err != nil { return } @@ -2477,3 +2399,50 @@ 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() (int64, error) { + return db.GetEngine(db.DefaultContext).Table("issue"). + Join("LEFT", "repository", "issue.repo_id=repository.id"). + Where(builder.IsNull{"repository.id"}). + Select("COUNT(`issue`.`id`)"). + Count() +} + +// DeleteOrphanedIssues delete issues without a repo +func DeleteOrphanedIssues() error { + ctx, committer, err := db.TxContext() + if err != nil { + return err + } + defer committer.Close() + + 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 + } + + var attachmentPaths []string + for i := range ids { + paths, err := DeleteIssuesByRepoID(ctx, ids[i]) + if err != nil { + return err + } + attachmentPaths = append(attachmentPaths, paths...) + } + + if err := committer.Commit(); err != nil { + return err + } + committer.Close() + + // Remove issue attachment files. + for i := range attachmentPaths { + admin_model.RemoveAllWithNotice(db.DefaultContext, "Delete issue attachment", attachmentPaths[i]) + } + return nil +} diff --git a/models/issues/issue_index.go b/models/issues/issue_index.go new file mode 100644 index 0000000000..100e814317 --- /dev/null +++ b/models/issues/issue_index.go @@ -0,0 +1,32 @@ +// Copyright 2017 The Gitea Authors. All rights reserved. +// Use of this source code is governed by a MIT-style +// license that can be found in the LICENSE file. + +package issues + +import "code.gitea.io/gitea/models/db" + +// RecalculateIssueIndexForRepo create issue_index for repo if not exist and +// update it based on highest index of existing issues assigned to a repo +func RecalculateIssueIndexForRepo(repoID int64) error { + ctx, committer, err := db.TxContext() + if err != nil { + return err + } + defer committer.Close() + + if err := db.UpsertResourceIndex(ctx, "issue_index", repoID); err != nil { + return err + } + + var max int64 + if _, err := db.GetEngine(ctx).Select(" MAX(`index`)").Table("issue").Where("repo_id=?", repoID).Get(&max); err != nil { + return err + } + + if _, err := db.GetEngine(ctx).Exec("UPDATE `issue_index` SET max_index=? WHERE group_id=?", max, repoID); err != nil { + return err + } + + return committer.Commit() +} diff --git a/models/issue_list.go b/models/issues/issue_list.go index a5fc095e12..20e9949b66 100644 --- a/models/issue_list.go +++ b/models/issues/issue_list.go @@ -2,14 +2,13 @@ // Use of this source code is governed by a MIT-style // license that can be found in the LICENSE file. -package models +package issues import ( "context" "fmt" "code.gitea.io/gitea/models/db" - issues_model "code.gitea.io/gitea/models/issues" repo_model "code.gitea.io/gitea/models/repo" user_model "code.gitea.io/gitea/models/user" "code.gitea.io/gitea/modules/container" @@ -20,11 +19,6 @@ import ( // IssueList defines a list of issues type IssueList []*Issue -const ( - // default variables number on IN () in SQL - defaultMaxInSize = 50 -) - // get the repo IDs to be loaded later, these IDs are for issue.Repo and issue.PullRequest.HeadRepo func (issues IssueList) getRepoIDs() []int64 { repoIDs := make(map[int64]struct{}, len(issues)) @@ -48,7 +42,7 @@ func (issues IssueList) loadRepositories(ctx context.Context) ([]*repo_model.Rep repoMaps := make(map[int64]*repo_model.Repository, len(repoIDs)) left := len(repoIDs) for left > 0 { - limit := defaultMaxInSize + limit := db.DefaultMaxInSize if left < limit { limit = left } @@ -102,7 +96,7 @@ func (issues IssueList) loadPosters(ctx context.Context) error { posterMaps := make(map[int64]*user_model.User, len(posterIDs)) left := len(posterIDs) for left > 0 { - limit := defaultMaxInSize + limit := db.DefaultMaxInSize if left < limit { limit = left } @@ -150,7 +144,7 @@ func (issues IssueList) loadLabels(ctx context.Context) error { issueIDs := issues.getIssueIDs() left := len(issueIDs) for left > 0 { - limit := defaultMaxInSize + limit := db.DefaultMaxInSize if left < limit { limit = left } @@ -205,10 +199,10 @@ func (issues IssueList) loadMilestones(ctx context.Context) error { return nil } - milestoneMaps := make(map[int64]*issues_model.Milestone, len(milestoneIDs)) + milestoneMaps := make(map[int64]*Milestone, len(milestoneIDs)) left := len(milestoneIDs) for left > 0 { - limit := defaultMaxInSize + limit := db.DefaultMaxInSize if left < limit { limit = left } @@ -242,7 +236,7 @@ func (issues IssueList) loadAssignees(ctx context.Context) error { issueIDs := issues.getIssueIDs() left := len(issueIDs) for left > 0 { - limit := defaultMaxInSize + limit := db.DefaultMaxInSize if left < limit { limit = left } @@ -298,7 +292,7 @@ func (issues IssueList) loadPullRequests(ctx context.Context) error { pullRequestMaps := make(map[int64]*PullRequest, len(issuesIDs)) left := len(issuesIDs) for left > 0 { - limit := defaultMaxInSize + limit := db.DefaultMaxInSize if left < limit { limit = left } @@ -342,7 +336,7 @@ func (issues IssueList) loadAttachments(ctx context.Context) (err error) { issuesIDs := issues.getIssueIDs() left := len(issuesIDs) for left > 0 { - limit := defaultMaxInSize + limit := db.DefaultMaxInSize if left < limit { limit = left } @@ -387,7 +381,7 @@ func (issues IssueList) loadComments(ctx context.Context, cond builder.Cond) (er issuesIDs := issues.getIssueIDs() left := len(issuesIDs) for left > 0 { - limit := defaultMaxInSize + limit := db.DefaultMaxInSize if left < limit { limit = left } @@ -443,7 +437,7 @@ func (issues IssueList) loadTotalTrackedTimes(ctx context.Context) (err error) { left := len(ids) for left > 0 { - limit := defaultMaxInSize + limit := db.DefaultMaxInSize if left < limit { limit = left } diff --git a/models/issue_list_test.go b/models/issues/issue_list_test.go index 2916a7302f..6b978f9ae6 100644 --- a/models/issue_list_test.go +++ b/models/issues/issue_list_test.go @@ -2,11 +2,12 @@ // Use of this source code is governed by a MIT-style // license that can be found in the LICENSE file. -package models +package issues_test import ( "testing" + issues_model "code.gitea.io/gitea/models/issues" "code.gitea.io/gitea/models/unittest" "code.gitea.io/gitea/modules/setting" @@ -16,10 +17,10 @@ import ( func TestIssueList_LoadRepositories(t *testing.T) { assert.NoError(t, unittest.PrepareTestDatabase()) - issueList := IssueList{ - unittest.AssertExistsAndLoadBean(t, &Issue{ID: 1}).(*Issue), - unittest.AssertExistsAndLoadBean(t, &Issue{ID: 2}).(*Issue), - unittest.AssertExistsAndLoadBean(t, &Issue{ID: 4}).(*Issue), + issueList := issues_model.IssueList{ + unittest.AssertExistsAndLoadBean(t, &issues_model.Issue{ID: 1}).(*issues_model.Issue), + unittest.AssertExistsAndLoadBean(t, &issues_model.Issue{ID: 2}).(*issues_model.Issue), + unittest.AssertExistsAndLoadBean(t, &issues_model.Issue{ID: 4}).(*issues_model.Issue), } repos, err := issueList.LoadRepositories() @@ -33,9 +34,9 @@ func TestIssueList_LoadRepositories(t *testing.T) { func TestIssueList_LoadAttributes(t *testing.T) { assert.NoError(t, unittest.PrepareTestDatabase()) setting.Service.EnableTimetracking = true - issueList := IssueList{ - unittest.AssertExistsAndLoadBean(t, &Issue{ID: 1}).(*Issue), - unittest.AssertExistsAndLoadBean(t, &Issue{ID: 4}).(*Issue), + issueList := issues_model.IssueList{ + unittest.AssertExistsAndLoadBean(t, &issues_model.Issue{ID: 1}).(*issues_model.Issue), + unittest.AssertExistsAndLoadBean(t, &issues_model.Issue{ID: 4}).(*issues_model.Issue), } assert.NoError(t, issueList.LoadAttributes()) @@ -43,7 +44,7 @@ func TestIssueList_LoadAttributes(t *testing.T) { assert.EqualValues(t, issue.RepoID, issue.Repo.ID) for _, label := range issue.Labels { assert.EqualValues(t, issue.RepoID, label.RepoID) - unittest.AssertExistsAndLoadBean(t, &IssueLabel{IssueID: issue.ID, LabelID: label.ID}) + unittest.AssertExistsAndLoadBean(t, &issues_model.IssueLabel{IssueID: issue.ID, LabelID: label.ID}) } if issue.PosterID > 0 { assert.EqualValues(t, issue.PosterID, issue.Poster.ID) diff --git a/models/issue_lock.go b/models/issues/issue_lock.go index a122f618d0..7b52429ef7 100644 --- a/models/issue_lock.go +++ b/models/issues/issue_lock.go @@ -2,7 +2,7 @@ // Use of this source code is governed by a MIT-style // license that can be found in the LICENSE file. -package models +package issues import ( "code.gitea.io/gitea/models/db" diff --git a/models/issue_project.go b/models/issues/issue_project.go index 0f8c61977b..5e0a337f7d 100644 --- a/models/issue_project.go +++ b/models/issues/issue_project.go @@ -2,7 +2,7 @@ // Use of this source code is governed by a MIT-style // license that can be found in the LICENSE file. -package models +package issues import ( "context" diff --git a/models/issue_test.go b/models/issues/issue_test.go index 5b2f461a84..019e578da8 100644 --- a/models/issue_test.go +++ b/models/issues/issue_test.go @@ -2,7 +2,7 @@ // Use of this source code is governed by a MIT-style // license that can be found in the LICENSE file. -package models +package issues_test import ( "context" @@ -29,18 +29,18 @@ func TestIssue_ReplaceLabels(t *testing.T) { assert.NoError(t, unittest.PrepareTestDatabase()) testSuccess := func(issueID int64, labelIDs []int64) { - issue := unittest.AssertExistsAndLoadBean(t, &Issue{ID: issueID}).(*Issue) + issue := unittest.AssertExistsAndLoadBean(t, &issues_model.Issue{ID: issueID}).(*issues_model.Issue) repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: issue.RepoID}).(*repo_model.Repository) doer := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: repo.OwnerID}).(*user_model.User) - labels := make([]*Label, len(labelIDs)) + labels := make([]*issues_model.Label, len(labelIDs)) for i, labelID := range labelIDs { - labels[i] = unittest.AssertExistsAndLoadBean(t, &Label{ID: labelID, RepoID: repo.ID}).(*Label) + labels[i] = unittest.AssertExistsAndLoadBean(t, &issues_model.Label{ID: labelID, RepoID: repo.ID}).(*issues_model.Label) } - assert.NoError(t, ReplaceIssueLabels(issue, labels, doer)) - unittest.AssertCount(t, &IssueLabel{IssueID: issueID}, len(labelIDs)) + assert.NoError(t, issues_model.ReplaceIssueLabels(issue, labels, doer)) + unittest.AssertCount(t, &issues_model.IssueLabel{IssueID: issueID}, len(labelIDs)) for _, labelID := range labelIDs { - unittest.AssertExistsAndLoadBean(t, &IssueLabel{IssueID: issueID, LabelID: labelID}) + unittest.AssertExistsAndLoadBean(t, &issues_model.IssueLabel{IssueID: issueID, LabelID: labelID}) } } @@ -52,15 +52,15 @@ func TestIssue_ReplaceLabels(t *testing.T) { func Test_GetIssueIDsByRepoID(t *testing.T) { assert.NoError(t, unittest.PrepareTestDatabase()) - ids, err := GetIssueIDsByRepoID(db.DefaultContext, 1) + ids, err := issues_model.GetIssueIDsByRepoID(db.DefaultContext, 1) assert.NoError(t, err) assert.Len(t, ids, 5) } func TestIssueAPIURL(t *testing.T) { assert.NoError(t, unittest.PrepareTestDatabase()) - issue := unittest.AssertExistsAndLoadBean(t, &Issue{ID: 1}).(*Issue) - err := issue.LoadAttributes() + issue := unittest.AssertExistsAndLoadBean(t, &issues_model.Issue{ID: 1}).(*issues_model.Issue) + err := issue.LoadAttributes(db.DefaultContext) assert.NoError(t, err) assert.Equal(t, "https://try.gitea.io/api/v1/repos/user2/repo1/issues/1", issue.APIURL()) @@ -69,7 +69,7 @@ func TestIssueAPIURL(t *testing.T) { func TestGetIssuesByIDs(t *testing.T) { assert.NoError(t, unittest.PrepareTestDatabase()) testSuccess := func(expectedIssueIDs, nonExistentIssueIDs []int64) { - issues, err := GetIssuesByIDs(db.DefaultContext, append(expectedIssueIDs, nonExistentIssueIDs...)) + issues, err := issues_model.GetIssuesByIDs(db.DefaultContext, append(expectedIssueIDs, nonExistentIssueIDs...)) assert.NoError(t, err) actualIssueIDs := make([]int64, len(issues)) for i, issue := range issues { @@ -85,9 +85,9 @@ func TestGetParticipantIDsByIssue(t *testing.T) { assert.NoError(t, unittest.PrepareTestDatabase()) checkParticipants := func(issueID int64, userIDs []int) { - issue, err := GetIssueByID(issueID) + issue, err := issues_model.GetIssueByID(db.DefaultContext, issueID) assert.NoError(t, err) - participants, err := issue.getParticipantIDsByIssue(db.DefaultContext) + participants, err := issue.GetParticipantIDsByIssue(db.DefaultContext) if assert.NoError(t, err) { participantsIDs := make([]int, len(participants)) for i, uid := range participants { @@ -117,16 +117,16 @@ func TestIssue_ClearLabels(t *testing.T) { } for _, test := range tests { assert.NoError(t, unittest.PrepareTestDatabase()) - issue := unittest.AssertExistsAndLoadBean(t, &Issue{ID: test.issueID}).(*Issue) + issue := unittest.AssertExistsAndLoadBean(t, &issues_model.Issue{ID: test.issueID}).(*issues_model.Issue) doer := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: test.doerID}).(*user_model.User) - assert.NoError(t, ClearIssueLabels(issue, doer)) - unittest.AssertNotExistsBean(t, &IssueLabel{IssueID: test.issueID}) + assert.NoError(t, issues_model.ClearIssueLabels(issue, doer)) + unittest.AssertNotExistsBean(t, &issues_model.IssueLabel{IssueID: test.issueID}) } } func TestUpdateIssueCols(t *testing.T) { assert.NoError(t, unittest.PrepareTestDatabase()) - issue := unittest.AssertExistsAndLoadBean(t, &Issue{}).(*Issue) + issue := unittest.AssertExistsAndLoadBean(t, &issues_model.Issue{}).(*issues_model.Issue) const newTitle = "New Title for unit test" issue.Title = newTitle @@ -135,10 +135,10 @@ func TestUpdateIssueCols(t *testing.T) { issue.Content = "This should have no effect" now := time.Now().Unix() - assert.NoError(t, UpdateIssueCols(db.DefaultContext, issue, "name")) + assert.NoError(t, issues_model.UpdateIssueCols(db.DefaultContext, issue, "name")) then := time.Now().Unix() - updatedIssue := unittest.AssertExistsAndLoadBean(t, &Issue{ID: issue.ID}).(*Issue) + updatedIssue := unittest.AssertExistsAndLoadBean(t, &issues_model.Issue{ID: issue.ID}).(*issues_model.Issue) assert.EqualValues(t, newTitle, updatedIssue.Title) assert.EqualValues(t, prevContent, updatedIssue.Content) unittest.AssertInt64InRange(t, now, then, int64(updatedIssue.UpdatedUnix)) @@ -147,18 +147,18 @@ func TestUpdateIssueCols(t *testing.T) { func TestIssues(t *testing.T) { assert.NoError(t, unittest.PrepareTestDatabase()) for _, test := range []struct { - Opts IssuesOptions + Opts issues_model.IssuesOptions ExpectedIssueIDs []int64 }{ { - IssuesOptions{ + issues_model.IssuesOptions{ AssigneeID: 1, SortType: "oldest", }, []int64{1, 6}, }, { - IssuesOptions{ + issues_model.IssuesOptions{ RepoCond: builder.In("repo_id", 1, 3), SortType: "oldest", ListOptions: db.ListOptions{ @@ -169,7 +169,7 @@ func TestIssues(t *testing.T) { []int64{1, 2, 3, 5}, }, { - IssuesOptions{ + issues_model.IssuesOptions{ LabelIDs: []int64{1}, ListOptions: db.ListOptions{ Page: 1, @@ -179,7 +179,7 @@ func TestIssues(t *testing.T) { []int64{2, 1}, }, { - IssuesOptions{ + issues_model.IssuesOptions{ LabelIDs: []int64{1, 2}, ListOptions: db.ListOptions{ Page: 1, @@ -189,7 +189,7 @@ func TestIssues(t *testing.T) { []int64{}, // issues with **both** label 1 and 2, none of these issues matches, TODO: add more tests }, } { - issues, err := Issues(&test.Opts) + issues, err := issues_model.Issues(&test.Opts) assert.NoError(t, err) if assert.Len(t, issues, len(test.ExpectedIssueIDs)) { for i, issue := range issues { @@ -202,16 +202,16 @@ func TestIssues(t *testing.T) { func TestGetUserIssueStats(t *testing.T) { assert.NoError(t, unittest.PrepareTestDatabase()) for _, test := range []struct { - Opts UserIssueStatsOptions - ExpectedIssueStats IssueStats + Opts issues_model.UserIssueStatsOptions + ExpectedIssueStats issues_model.IssueStats }{ { - UserIssueStatsOptions{ + issues_model.UserIssueStatsOptions{ UserID: 1, RepoIDs: []int64{1}, - FilterMode: FilterModeAll, + FilterMode: issues_model.FilterModeAll, }, - IssueStats{ + issues_model.IssueStats{ YourRepositoriesCount: 1, // 6 AssignCount: 1, // 6 CreateCount: 1, // 6 @@ -220,13 +220,13 @@ func TestGetUserIssueStats(t *testing.T) { }, }, { - UserIssueStatsOptions{ + issues_model.UserIssueStatsOptions{ UserID: 1, RepoIDs: []int64{1}, - FilterMode: FilterModeAll, + FilterMode: issues_model.FilterModeAll, IsClosed: true, }, - IssueStats{ + issues_model.IssueStats{ YourRepositoriesCount: 1, // 6 AssignCount: 0, CreateCount: 0, @@ -235,11 +235,11 @@ func TestGetUserIssueStats(t *testing.T) { }, }, { - UserIssueStatsOptions{ + issues_model.UserIssueStatsOptions{ UserID: 1, - FilterMode: FilterModeAssign, + FilterMode: issues_model.FilterModeAssign, }, - IssueStats{ + issues_model.IssueStats{ YourRepositoriesCount: 1, // 6 AssignCount: 1, // 6 CreateCount: 1, // 6 @@ -248,11 +248,11 @@ func TestGetUserIssueStats(t *testing.T) { }, }, { - UserIssueStatsOptions{ + issues_model.UserIssueStatsOptions{ UserID: 1, - FilterMode: FilterModeCreate, + FilterMode: issues_model.FilterModeCreate, }, - IssueStats{ + issues_model.IssueStats{ YourRepositoriesCount: 1, // 6 AssignCount: 1, // 6 CreateCount: 1, // 6 @@ -261,11 +261,11 @@ func TestGetUserIssueStats(t *testing.T) { }, }, { - UserIssueStatsOptions{ + issues_model.UserIssueStatsOptions{ UserID: 1, - FilterMode: FilterModeMention, + FilterMode: issues_model.FilterModeMention, }, - IssueStats{ + issues_model.IssueStats{ YourRepositoriesCount: 1, // 6 AssignCount: 1, // 6 CreateCount: 1, // 6 @@ -275,12 +275,12 @@ func TestGetUserIssueStats(t *testing.T) { }, }, { - UserIssueStatsOptions{ + issues_model.UserIssueStatsOptions{ UserID: 1, - FilterMode: FilterModeCreate, + FilterMode: issues_model.FilterModeCreate, IssueIDs: []int64{1}, }, - IssueStats{ + issues_model.IssueStats{ YourRepositoriesCount: 1, // 1 AssignCount: 1, // 1 CreateCount: 1, // 1 @@ -289,13 +289,13 @@ func TestGetUserIssueStats(t *testing.T) { }, }, { - UserIssueStatsOptions{ + issues_model.UserIssueStatsOptions{ UserID: 2, Org: unittest.AssertExistsAndLoadBean(t, &organization.Organization{ID: 3}).(*organization.Organization), Team: unittest.AssertExistsAndLoadBean(t, &organization.Team{ID: 7}).(*organization.Team), - FilterMode: FilterModeAll, + FilterMode: issues_model.FilterModeAll, }, - IssueStats{ + issues_model.IssueStats{ YourRepositoriesCount: 2, AssignCount: 1, CreateCount: 1, @@ -304,7 +304,7 @@ func TestGetUserIssueStats(t *testing.T) { }, } { t.Run(fmt.Sprintf("%#v", test.Opts), func(t *testing.T) { - stats, err := GetUserIssueStats(test.Opts) + stats, err := issues_model.GetUserIssueStats(test.Opts) if !assert.NoError(t, err) { return } @@ -315,31 +315,31 @@ func TestGetUserIssueStats(t *testing.T) { func TestIssue_loadTotalTimes(t *testing.T) { assert.NoError(t, unittest.PrepareTestDatabase()) - ms, err := GetIssueByID(2) + ms, err := issues_model.GetIssueByID(db.DefaultContext, 2) assert.NoError(t, err) - assert.NoError(t, ms.loadTotalTimes(db.DefaultContext)) + assert.NoError(t, ms.LoadTotalTimes(db.DefaultContext)) assert.Equal(t, int64(3682), ms.TotalTrackedTime) } func TestIssue_SearchIssueIDsByKeyword(t *testing.T) { assert.NoError(t, unittest.PrepareTestDatabase()) - total, ids, err := SearchIssueIDsByKeyword(context.TODO(), "issue2", []int64{1}, 10, 0) + total, ids, err := issues_model.SearchIssueIDsByKeyword(context.TODO(), "issue2", []int64{1}, 10, 0) assert.NoError(t, err) assert.EqualValues(t, 1, total) assert.EqualValues(t, []int64{2}, ids) - total, ids, err = SearchIssueIDsByKeyword(context.TODO(), "first", []int64{1}, 10, 0) + total, ids, err = issues_model.SearchIssueIDsByKeyword(context.TODO(), "first", []int64{1}, 10, 0) assert.NoError(t, err) assert.EqualValues(t, 1, total) assert.EqualValues(t, []int64{1}, ids) - total, ids, err = SearchIssueIDsByKeyword(context.TODO(), "for", []int64{1}, 10, 0) + total, ids, err = issues_model.SearchIssueIDsByKeyword(context.TODO(), "for", []int64{1}, 10, 0) assert.NoError(t, err) assert.EqualValues(t, 5, total) assert.ElementsMatch(t, []int64{1, 2, 3, 5, 11}, ids) // issue1's comment id 2 - total, ids, err = SearchIssueIDsByKeyword(context.TODO(), "good", []int64{1}, 10, 0) + total, ids, err = issues_model.SearchIssueIDsByKeyword(context.TODO(), "good", []int64{1}, 10, 0) assert.NoError(t, err) assert.EqualValues(t, 1, total) assert.EqualValues(t, []int64{1}, ids) @@ -349,23 +349,23 @@ func TestGetRepoIDsForIssuesOptions(t *testing.T) { assert.NoError(t, unittest.PrepareTestDatabase()) user := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2}).(*user_model.User) for _, test := range []struct { - Opts IssuesOptions + Opts issues_model.IssuesOptions ExpectedRepoIDs []int64 }{ { - IssuesOptions{ + issues_model.IssuesOptions{ AssigneeID: 2, }, []int64{3, 32}, }, { - IssuesOptions{ + issues_model.IssuesOptions{ RepoCond: builder.In("repo_id", 1, 2), }, []int64{1, 2}, }, } { - repoIDs, err := GetRepoIDsForIssuesOptions(&test.Opts, user) + repoIDs, err := issues_model.GetRepoIDsForIssuesOptions(&test.Opts, user) assert.NoError(t, err) if assert.Len(t, repoIDs, len(test.ExpectedRepoIDs)) { for i, repoID := range repoIDs { @@ -375,20 +375,20 @@ func TestGetRepoIDsForIssuesOptions(t *testing.T) { } } -func testInsertIssue(t *testing.T, title, content string, expectIndex int64) *Issue { - var newIssue Issue +func testInsertIssue(t *testing.T, title, content string, expectIndex int64) *issues_model.Issue { + var newIssue issues_model.Issue t.Run(title, func(t *testing.T) { repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 1}).(*repo_model.Repository) user := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2}).(*user_model.User) - issue := Issue{ + issue := issues_model.Issue{ RepoID: repo.ID, PosterID: user.ID, Poster: user, Title: title, Content: content, } - err := NewIssue(repo, &issue, nil, nil) + err := issues_model.NewIssue(repo, &issue, nil, nil) assert.NoError(t, err) has, err := db.GetEngine(db.DefaultContext).ID(issue.ID).Get(&newIssue) @@ -408,75 +408,23 @@ func TestIssue_InsertIssue(t *testing.T) { // there are 5 issues and max index is 5 on repository 1, so this one should 6 issue := testInsertIssue(t, "my issue1", "special issue's comments?", 6) - _, err := db.GetEngine(db.DefaultContext).ID(issue.ID).Delete(new(Issue)) + _, err := db.GetEngine(db.DefaultContext).ID(issue.ID).Delete(new(issues_model.Issue)) assert.NoError(t, err) issue = testInsertIssue(t, `my issue2, this is my son's love \n \r \ `, "special issue's '' comments?", 7) - _, err = db.GetEngine(db.DefaultContext).ID(issue.ID).Delete(new(Issue)) + _, err = db.GetEngine(db.DefaultContext).ID(issue.ID).Delete(new(issues_model.Issue)) assert.NoError(t, err) } -func TestIssue_DeleteIssue(t *testing.T) { - assert.NoError(t, unittest.PrepareTestDatabase()) - - issueIDs, err := GetIssueIDsByRepoID(db.DefaultContext, 1) - assert.NoError(t, err) - assert.EqualValues(t, 5, len(issueIDs)) - - issue := &Issue{ - RepoID: 1, - ID: issueIDs[2], - } - - err = DeleteIssue(issue) - assert.NoError(t, err) - issueIDs, err = GetIssueIDsByRepoID(db.DefaultContext, 1) - assert.NoError(t, err) - assert.EqualValues(t, 4, len(issueIDs)) - - // check attachment removal - attachments, err := repo_model.GetAttachmentsByIssueID(db.DefaultContext, 4) - assert.NoError(t, err) - issue, err = GetIssueByID(4) - assert.NoError(t, err) - err = DeleteIssue(issue) - assert.NoError(t, err) - assert.EqualValues(t, 2, len(attachments)) - for i := range attachments { - attachment, err := repo_model.GetAttachmentByUUID(db.DefaultContext, attachments[i].UUID) - assert.Error(t, err) - assert.True(t, repo_model.IsErrAttachmentNotExist(err)) - assert.Nil(t, attachment) - } - - // check issue dependencies - user, err := user_model.GetUserByID(1) - assert.NoError(t, err) - issue1, err := GetIssueByID(1) - assert.NoError(t, err) - issue2, err := GetIssueByID(2) - assert.NoError(t, err) - err = CreateIssueDependency(user, issue1, issue2) - assert.NoError(t, err) - left, err := IssueNoDependenciesLeft(db.DefaultContext, issue1) - assert.NoError(t, err) - assert.False(t, left) - err = DeleteIssue(&Issue{ID: 2}) - assert.NoError(t, err) - left, err = IssueNoDependenciesLeft(db.DefaultContext, issue1) - assert.NoError(t, err) - assert.True(t, left) -} - func TestIssue_ResolveMentions(t *testing.T) { assert.NoError(t, unittest.PrepareTestDatabase()) testSuccess := func(owner, repo, doer string, mentions []string, expected []int64) { o := unittest.AssertExistsAndLoadBean(t, &user_model.User{LowerName: owner}).(*user_model.User) r := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{OwnerID: o.ID, LowerName: repo}).(*repo_model.Repository) - issue := &Issue{RepoID: r.ID} + issue := &issues_model.Issue{RepoID: r.ID} d := unittest.AssertExistsAndLoadBean(t, &user_model.User{LowerName: doer}).(*user_model.User) - resolved, err := ResolveIssueMentionsByVisibility(db.DefaultContext, issue, d, mentions) + resolved, err := issues_model.ResolveIssueMentionsByVisibility(db.DefaultContext, issue, d, mentions) assert.NoError(t, err) ids := make([]int64, len(resolved)) for i, user := range resolved { @@ -523,7 +471,7 @@ func TestCorrectIssueStats(t *testing.T) { // Each new issues will have a constant description "Bugs are nasty" // Which will be used later on. - issueAmount := maxQueryParameters + 10 + issueAmount := issues_model.MaxQueryParameters + 10 var wg sync.WaitGroup for i := 0; i < issueAmount; i++ { @@ -536,7 +484,7 @@ func TestCorrectIssueStats(t *testing.T) { wg.Wait() // Now we will get all issueID's that match the "Bugs are nasty" query. - total, ids, err := SearchIssueIDsByKeyword(context.TODO(), "Bugs are nasty", []int64{1}, issueAmount, 0) + total, ids, err := issues_model.SearchIssueIDsByKeyword(context.TODO(), "Bugs are nasty", []int64{1}, issueAmount, 0) // Just to be sure. assert.NoError(t, err) @@ -544,7 +492,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 := GetIssueStats(&IssueStatsOptions{ + issueStats, err := issues_model.GetIssueStats(&issues_model.IssueStatsOptions{ RepoID: 1, IssueIDs: ids, }) @@ -556,19 +504,19 @@ func TestCorrectIssueStats(t *testing.T) { func TestIssueForeignReference(t *testing.T) { assert.NoError(t, unittest.PrepareTestDatabase()) - issue := unittest.AssertExistsAndLoadBean(t, &Issue{ID: 4}).(*Issue) + issue := unittest.AssertExistsAndLoadBean(t, &issues_model.Issue{ID: 4}).(*issues_model.Issue) assert.NotEqualValues(t, issue.Index, issue.ID) // make sure they are different to avoid false positive // it is fine for an issue to not have a foreign reference - err := issue.LoadAttributes() + err := issue.LoadAttributes(db.DefaultContext) assert.NoError(t, err) assert.Nil(t, issue.ForeignReference) var foreignIndex int64 = 12345 - _, err = GetIssueByForeignIndex(context.Background(), issue.RepoID, foreignIndex) + _, err = issues_model.GetIssueByForeignIndex(context.Background(), issue.RepoID, foreignIndex) assert.True(t, foreignreference.IsErrLocalIndexNotExist(err)) - _, err = db.GetEngine(db.DefaultContext).Insert(&foreignreference.ForeignReference{ + err = db.Insert(db.DefaultContext, &foreignreference.ForeignReference{ LocalIndex: issue.Index, ForeignIndex: strconv.FormatInt(foreignIndex, 10), RepoID: issue.RepoID, @@ -576,12 +524,12 @@ func TestIssueForeignReference(t *testing.T) { }) assert.NoError(t, err) - err = issue.LoadAttributes() + err = issue.LoadAttributes(db.DefaultContext) assert.NoError(t, err) assert.EqualValues(t, issue.ForeignReference.ForeignIndex, strconv.FormatInt(foreignIndex, 10)) - found, err := GetIssueByForeignIndex(context.Background(), issue.RepoID, foreignIndex) + found, err := issues_model.GetIssueByForeignIndex(context.Background(), issue.RepoID, foreignIndex) assert.NoError(t, err) assert.EqualValues(t, found.Index, issue.Index) } @@ -608,7 +556,7 @@ func TestLoadTotalTrackedTime(t *testing.T) { func TestCountIssues(t *testing.T) { assert.NoError(t, unittest.PrepareTestDatabase()) - count, err := CountIssues(&IssuesOptions{}) + count, err := issues_model.CountIssues(&issues_model.IssuesOptions{}) assert.NoError(t, err) assert.EqualValues(t, 17, count) } diff --git a/models/issue_user.go b/models/issues/issue_user.go index 19c64094a1..f5d22589af 100644 --- a/models/issue_user.go +++ b/models/issues/issue_user.go @@ -2,7 +2,7 @@ // Use of this source code is governed by a MIT-style // license that can be found in the LICENSE file. -package models +package issues import ( "context" @@ -25,7 +25,8 @@ func init() { db.RegisterModel(new(IssueUser)) } -func newIssueUsers(ctx context.Context, repo *repo_model.Repository, issue *Issue) error { +// NewIssueUsers inserts an issue related users +func NewIssueUsers(ctx context.Context, repo *repo_model.Repository, issue *Issue) error { assignees, err := repo_model.GetRepoAssignees(ctx, repo) if err != nil { return fmt.Errorf("getAssignees: %v", err) diff --git a/models/issues/issue_user_test.go b/models/issues/issue_user_test.go new file mode 100644 index 0000000000..33e9f98ecc --- /dev/null +++ b/models/issues/issue_user_test.go @@ -0,0 +1,62 @@ +// Copyright 2017 The Gogs Authors. All rights reserved. +// Use of this source code is governed by a MIT-style +// license that can be found in the LICENSE file. + +package issues_test + +import ( + "testing" + + "code.gitea.io/gitea/models/db" + issues_model "code.gitea.io/gitea/models/issues" + repo_model "code.gitea.io/gitea/models/repo" + "code.gitea.io/gitea/models/unittest" + + "github.com/stretchr/testify/assert" +) + +func Test_NewIssueUsers(t *testing.T) { + assert.NoError(t, unittest.PrepareTestDatabase()) + + repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 1}).(*repo_model.Repository) + newIssue := &issues_model.Issue{ + RepoID: repo.ID, + PosterID: 4, + Index: 6, + Title: "newTestIssueTitle", + Content: "newTestIssueContent", + } + + // artificially insert new issue + unittest.AssertSuccessfulInsert(t, newIssue) + + assert.NoError(t, issues_model.NewIssueUsers(db.DefaultContext, repo, newIssue)) + + // issue_user table should now have entries for new issue + unittest.AssertExistsAndLoadBean(t, &issues_model.IssueUser{IssueID: newIssue.ID, UID: newIssue.PosterID}) + unittest.AssertExistsAndLoadBean(t, &issues_model.IssueUser{IssueID: newIssue.ID, UID: repo.OwnerID}) +} + +func TestUpdateIssueUserByRead(t *testing.T) { + assert.NoError(t, unittest.PrepareTestDatabase()) + issue := unittest.AssertExistsAndLoadBean(t, &issues_model.Issue{ID: 1}).(*issues_model.Issue) + + assert.NoError(t, issues_model.UpdateIssueUserByRead(4, issue.ID)) + unittest.AssertExistsAndLoadBean(t, &issues_model.IssueUser{IssueID: issue.ID, UID: 4}, "is_read=1") + + assert.NoError(t, issues_model.UpdateIssueUserByRead(4, issue.ID)) + unittest.AssertExistsAndLoadBean(t, &issues_model.IssueUser{IssueID: issue.ID, UID: 4}, "is_read=1") + + assert.NoError(t, issues_model.UpdateIssueUserByRead(unittest.NonexistentID, unittest.NonexistentID)) +} + +func TestUpdateIssueUsersByMentions(t *testing.T) { + assert.NoError(t, unittest.PrepareTestDatabase()) + issue := unittest.AssertExistsAndLoadBean(t, &issues_model.Issue{ID: 1}).(*issues_model.Issue) + + uids := []int64{2, 5} + assert.NoError(t, issues_model.UpdateIssueUsersByMentions(db.DefaultContext, issue.ID, uids)) + for _, uid := range uids { + unittest.AssertExistsAndLoadBean(t, &issues_model.IssueUser{IssueID: issue.ID, UID: uid}, "is_mentioned=1") + } +} diff --git a/models/issue_watch.go b/models/issues/issue_watch.go index 9f41d36e19..bf907aa8fd 100644 --- a/models/issue_watch.go +++ b/models/issues/issue_watch.go @@ -2,7 +2,7 @@ // Use of this source code is governed by a MIT-style // license that can be found in the LICENSE file. -package models +package issues import ( "context" @@ -125,7 +125,8 @@ func CountIssueWatchers(ctx context.Context, issueID int64) (int64, error) { Join("INNER", "`user`", "`user`.id = `issue_watch`.user_id").Count(new(IssueWatch)) } -func removeIssueWatchersByRepoID(ctx context.Context, userID, repoID int64) error { +// RemoveIssueWatchersByRepoID remove issue watchers by repoID +func RemoveIssueWatchersByRepoID(ctx context.Context, userID, repoID int64) error { _, err := db.GetEngine(ctx). Join("INNER", "issue", "`issue`.id = `issue_watch`.issue_id AND `issue`.repo_id = ?", repoID). Where("`issue_watch`.user_id = ?", userID). diff --git a/models/issue_watch_test.go b/models/issues/issue_watch_test.go index b686196ae1..c6b6416d9b 100644 --- a/models/issue_watch_test.go +++ b/models/issues/issue_watch_test.go @@ -2,12 +2,13 @@ // Use of this source code is governed by a MIT-style // license that can be found in the LICENSE file. -package models +package issues_test import ( "testing" "code.gitea.io/gitea/models/db" + issues_model "code.gitea.io/gitea/models/issues" "code.gitea.io/gitea/models/unittest" "github.com/stretchr/testify/assert" @@ -16,28 +17,28 @@ import ( func TestCreateOrUpdateIssueWatch(t *testing.T) { assert.NoError(t, unittest.PrepareTestDatabase()) - assert.NoError(t, CreateOrUpdateIssueWatch(3, 1, true)) - iw := unittest.AssertExistsAndLoadBean(t, &IssueWatch{UserID: 3, IssueID: 1}).(*IssueWatch) + assert.NoError(t, issues_model.CreateOrUpdateIssueWatch(3, 1, true)) + iw := unittest.AssertExistsAndLoadBean(t, &issues_model.IssueWatch{UserID: 3, IssueID: 1}).(*issues_model.IssueWatch) assert.True(t, iw.IsWatching) - assert.NoError(t, CreateOrUpdateIssueWatch(1, 1, false)) - iw = unittest.AssertExistsAndLoadBean(t, &IssueWatch{UserID: 1, IssueID: 1}).(*IssueWatch) + assert.NoError(t, issues_model.CreateOrUpdateIssueWatch(1, 1, false)) + iw = unittest.AssertExistsAndLoadBean(t, &issues_model.IssueWatch{UserID: 1, IssueID: 1}).(*issues_model.IssueWatch) assert.False(t, iw.IsWatching) } func TestGetIssueWatch(t *testing.T) { assert.NoError(t, unittest.PrepareTestDatabase()) - _, exists, err := GetIssueWatch(db.DefaultContext, 9, 1) + _, exists, err := issues_model.GetIssueWatch(db.DefaultContext, 9, 1) assert.True(t, exists) assert.NoError(t, err) - iw, exists, err := GetIssueWatch(db.DefaultContext, 2, 2) + iw, exists, err := issues_model.GetIssueWatch(db.DefaultContext, 2, 2) assert.True(t, exists) assert.NoError(t, err) assert.False(t, iw.IsWatching) - _, exists, err = GetIssueWatch(db.DefaultContext, 3, 1) + _, exists, err = issues_model.GetIssueWatch(db.DefaultContext, 3, 1) assert.False(t, exists) assert.NoError(t, err) } @@ -45,22 +46,22 @@ func TestGetIssueWatch(t *testing.T) { func TestGetIssueWatchers(t *testing.T) { assert.NoError(t, unittest.PrepareTestDatabase()) - iws, err := GetIssueWatchers(db.DefaultContext, 1, db.ListOptions{}) + iws, err := issues_model.GetIssueWatchers(db.DefaultContext, 1, db.ListOptions{}) assert.NoError(t, err) // Watcher is inactive, thus 0 assert.Len(t, iws, 0) - iws, err = GetIssueWatchers(db.DefaultContext, 2, db.ListOptions{}) + iws, err = issues_model.GetIssueWatchers(db.DefaultContext, 2, db.ListOptions{}) assert.NoError(t, err) // Watcher is explicit not watching assert.Len(t, iws, 0) - iws, err = GetIssueWatchers(db.DefaultContext, 5, db.ListOptions{}) + iws, err = issues_model.GetIssueWatchers(db.DefaultContext, 5, db.ListOptions{}) assert.NoError(t, err) // Issue has no Watchers assert.Len(t, iws, 0) - iws, err = GetIssueWatchers(db.DefaultContext, 7, db.ListOptions{}) + iws, err = issues_model.GetIssueWatchers(db.DefaultContext, 7, db.ListOptions{}) assert.NoError(t, err) // Issue has one watcher assert.Len(t, iws, 1) diff --git a/models/issue_xref.go b/models/issues/issue_xref.go index 0c1623b5a4..f4380a02ec 100644 --- a/models/issue_xref.go +++ b/models/issues/issue_xref.go @@ -2,7 +2,7 @@ // Use of this source code is governed by a MIT-style // license that can be found in the LICENSE file. -package models +package issues import ( "context" @@ -55,15 +55,8 @@ func neuterCrossReferencesIds(ctx context.Context, ids []int64) error { return err } -// .___ -// | | ______ ________ __ ____ -// | |/ ___// ___/ | \_/ __ \ -// | |\___ \ \___ \| | /\ ___/ -// |___/____ >____ >____/ \___ > -// \/ \/ \/ -// - -func (issue *Issue) addCrossReferences(stdCtx context.Context, doer *user_model.User, removeOld bool) error { +// AddCrossReferences add cross repositories references. +func (issue *Issue) AddCrossReferences(stdCtx context.Context, doer *user_model.User, removeOld bool) error { var commentType CommentType if issue.IsPull { commentType = CommentTypePullRef @@ -237,15 +230,8 @@ func (issue *Issue) verifyReferencedIssue(stdCtx context.Context, ctx *crossRefe return refIssue, refAction, nil } -// _________ __ -// \_ ___ \ ____ _____ _____ ____ _____/ |_ -// / \ \/ / _ \ / \ / \_/ __ \ / \ __\ -// \ \___( <_> ) Y Y \ Y Y \ ___/| | \ | -// \______ /\____/|__|_| /__|_| /\___ >___| /__| -// \/ \/ \/ \/ \/ -// - -func (comment *Comment) addCrossReferences(stdCtx context.Context, doer *user_model.User, removeOld bool) error { +// AddCrossReferences add cross references +func (comment *Comment) AddCrossReferences(stdCtx context.Context, doer *user_model.User, removeOld bool) error { if comment.Type != CommentTypeCode && comment.Type != CommentTypeComment { return nil } @@ -280,7 +266,7 @@ func (comment *Comment) LoadRefIssue() (err error) { if comment.RefIssue != nil { return nil } - comment.RefIssue, err = GetIssueByID(comment.RefIssueID) + comment.RefIssue, err = GetIssueByID(db.DefaultContext, comment.RefIssueID) if err == nil { err = comment.RefIssue.LoadRepo(db.DefaultContext) } diff --git a/models/issue_xref_test.go b/models/issues/issue_xref_test.go index b4ad5b2708..6bb19d5328 100644 --- a/models/issue_xref_test.go +++ b/models/issues/issue_xref_test.go @@ -2,13 +2,14 @@ // Use of this source code is governed by a MIT-style // license that can be found in the LICENSE file. -package models +package issues_test import ( "fmt" "testing" "code.gitea.io/gitea/models/db" + issues_model "code.gitea.io/gitea/models/issues" repo_model "code.gitea.io/gitea/models/repo" "code.gitea.io/gitea/models/unittest" user_model "code.gitea.io/gitea/models/user" @@ -26,8 +27,8 @@ func TestXRef_AddCrossReferences(t *testing.T) { // PR to close issue #1 content := fmt.Sprintf("content2, closes #%d", itarget.Index) pr := testCreateIssue(t, 1, 2, "title2", content, true) - ref := unittest.AssertExistsAndLoadBean(t, &Comment{IssueID: itarget.ID, RefIssueID: pr.ID, RefCommentID: 0}).(*Comment) - assert.Equal(t, CommentTypePullRef, ref.Type) + ref := unittest.AssertExistsAndLoadBean(t, &issues_model.Comment{IssueID: itarget.ID, RefIssueID: pr.ID, RefCommentID: 0}).(*issues_model.Comment) + assert.Equal(t, issues_model.CommentTypePullRef, ref.Type) assert.Equal(t, pr.RepoID, ref.RefRepoID) assert.True(t, ref.RefIsPull) assert.Equal(t, references.XRefActionCloses, ref.RefAction) @@ -35,8 +36,8 @@ func TestXRef_AddCrossReferences(t *testing.T) { // Comment on PR to reopen issue #1 content = fmt.Sprintf("content2, reopens #%d", itarget.Index) c := testCreateComment(t, 1, 2, pr.ID, content) - ref = unittest.AssertExistsAndLoadBean(t, &Comment{IssueID: itarget.ID, RefIssueID: pr.ID, RefCommentID: c.ID}).(*Comment) - assert.Equal(t, CommentTypeCommentRef, ref.Type) + ref = unittest.AssertExistsAndLoadBean(t, &issues_model.Comment{IssueID: itarget.ID, RefIssueID: pr.ID, RefCommentID: c.ID}).(*issues_model.Comment) + assert.Equal(t, issues_model.CommentTypeCommentRef, ref.Type) assert.Equal(t, pr.RepoID, ref.RefRepoID) assert.True(t, ref.RefIsPull) assert.Equal(t, references.XRefActionReopens, ref.RefAction) @@ -44,8 +45,8 @@ func TestXRef_AddCrossReferences(t *testing.T) { // Issue mentioning issue #1 content = fmt.Sprintf("content3, mentions #%d", itarget.Index) i := testCreateIssue(t, 1, 2, "title3", content, false) - ref = unittest.AssertExistsAndLoadBean(t, &Comment{IssueID: itarget.ID, RefIssueID: i.ID, RefCommentID: 0}).(*Comment) - assert.Equal(t, CommentTypeIssueRef, ref.Type) + ref = unittest.AssertExistsAndLoadBean(t, &issues_model.Comment{IssueID: itarget.ID, RefIssueID: i.ID, RefCommentID: 0}).(*issues_model.Comment) + assert.Equal(t, issues_model.CommentTypeIssueRef, ref.Type) assert.Equal(t, pr.RepoID, ref.RefRepoID) assert.False(t, ref.RefIsPull) assert.Equal(t, references.XRefActionNone, ref.RefAction) @@ -56,8 +57,8 @@ func TestXRef_AddCrossReferences(t *testing.T) { // Cross-reference to issue #4 by admin content = fmt.Sprintf("content5, mentions user3/repo3#%d", itarget.Index) i = testCreateIssue(t, 2, 1, "title5", content, false) - ref = unittest.AssertExistsAndLoadBean(t, &Comment{IssueID: itarget.ID, RefIssueID: i.ID, RefCommentID: 0}).(*Comment) - assert.Equal(t, CommentTypeIssueRef, ref.Type) + ref = unittest.AssertExistsAndLoadBean(t, &issues_model.Comment{IssueID: itarget.ID, RefIssueID: i.ID, RefCommentID: 0}).(*issues_model.Comment) + assert.Equal(t, issues_model.CommentTypeIssueRef, ref.Type) assert.Equal(t, i.RepoID, ref.RefRepoID) assert.False(t, ref.RefIsPull) assert.Equal(t, references.XRefActionNone, ref.RefAction) @@ -65,7 +66,7 @@ func TestXRef_AddCrossReferences(t *testing.T) { // Cross-reference to issue #4 with no permission content = fmt.Sprintf("content6, mentions user3/repo3#%d", itarget.Index) i = testCreateIssue(t, 4, 5, "title6", content, false) - unittest.AssertNotExistsBean(t, &Comment{IssueID: itarget.ID, RefIssueID: i.ID, RefCommentID: 0}) + unittest.AssertNotExistsBean(t, &issues_model.Comment{IssueID: itarget.ID, RefIssueID: i.ID, RefCommentID: 0}) } func TestXRef_NeuterCrossReferences(t *testing.T) { @@ -77,16 +78,16 @@ func TestXRef_NeuterCrossReferences(t *testing.T) { // Issue mentioning issue #1 title := fmt.Sprintf("title2, mentions #%d", itarget.Index) i := testCreateIssue(t, 1, 2, title, "content2", false) - ref := unittest.AssertExistsAndLoadBean(t, &Comment{IssueID: itarget.ID, RefIssueID: i.ID, RefCommentID: 0}).(*Comment) - assert.Equal(t, CommentTypeIssueRef, ref.Type) + ref := unittest.AssertExistsAndLoadBean(t, &issues_model.Comment{IssueID: itarget.ID, RefIssueID: i.ID, RefCommentID: 0}).(*issues_model.Comment) + assert.Equal(t, issues_model.CommentTypeIssueRef, ref.Type) assert.Equal(t, references.XRefActionNone, ref.RefAction) d := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2}).(*user_model.User) i.Title = "title2, no mentions" - assert.NoError(t, ChangeIssueTitle(i, d, title)) + assert.NoError(t, issues_model.ChangeIssueTitle(i, d, title)) - ref = unittest.AssertExistsAndLoadBean(t, &Comment{IssueID: itarget.ID, RefIssueID: i.ID, RefCommentID: 0}).(*Comment) - assert.Equal(t, CommentTypeIssueRef, ref.Type) + ref = unittest.AssertExistsAndLoadBean(t, &issues_model.Comment{IssueID: itarget.ID, RefIssueID: i.ID, RefCommentID: 0}).(*issues_model.Comment) + assert.Equal(t, issues_model.CommentTypeIssueRef, ref.Type) assert.Equal(t, references.XRefActionNeutered, ref.RefAction) } @@ -98,25 +99,25 @@ func TestXRef_ResolveCrossReferences(t *testing.T) { i1 := testCreateIssue(t, 1, 2, "title1", "content1", false) i2 := testCreateIssue(t, 1, 2, "title2", "content2", false) i3 := testCreateIssue(t, 1, 2, "title3", "content3", false) - _, err := ChangeIssueStatus(db.DefaultContext, i3, d, true) + _, err := issues_model.ChangeIssueStatus(db.DefaultContext, i3, d, true) assert.NoError(t, err) pr := testCreatePR(t, 1, 2, "titlepr", fmt.Sprintf("closes #%d", i1.Index)) - rp := unittest.AssertExistsAndLoadBean(t, &Comment{IssueID: i1.ID, RefIssueID: pr.Issue.ID, RefCommentID: 0}).(*Comment) + rp := unittest.AssertExistsAndLoadBean(t, &issues_model.Comment{IssueID: i1.ID, RefIssueID: pr.Issue.ID, RefCommentID: 0}).(*issues_model.Comment) c1 := testCreateComment(t, 1, 2, pr.Issue.ID, fmt.Sprintf("closes #%d", i2.Index)) - r1 := unittest.AssertExistsAndLoadBean(t, &Comment{IssueID: i2.ID, RefIssueID: pr.Issue.ID, RefCommentID: c1.ID}).(*Comment) + r1 := unittest.AssertExistsAndLoadBean(t, &issues_model.Comment{IssueID: i2.ID, RefIssueID: pr.Issue.ID, RefCommentID: c1.ID}).(*issues_model.Comment) // Must be ignored c2 := testCreateComment(t, 1, 2, pr.Issue.ID, fmt.Sprintf("mentions #%d", i2.Index)) - unittest.AssertExistsAndLoadBean(t, &Comment{IssueID: i2.ID, RefIssueID: pr.Issue.ID, RefCommentID: c2.ID}) + unittest.AssertExistsAndLoadBean(t, &issues_model.Comment{IssueID: i2.ID, RefIssueID: pr.Issue.ID, RefCommentID: c2.ID}) // Must be superseded by c4/r4 c3 := testCreateComment(t, 1, 2, pr.Issue.ID, fmt.Sprintf("reopens #%d", i3.Index)) - unittest.AssertExistsAndLoadBean(t, &Comment{IssueID: i3.ID, RefIssueID: pr.Issue.ID, RefCommentID: c3.ID}) + unittest.AssertExistsAndLoadBean(t, &issues_model.Comment{IssueID: i3.ID, RefIssueID: pr.Issue.ID, RefCommentID: c3.ID}) c4 := testCreateComment(t, 1, 2, pr.Issue.ID, fmt.Sprintf("closes #%d", i3.Index)) - r4 := unittest.AssertExistsAndLoadBean(t, &Comment{IssueID: i3.ID, RefIssueID: pr.Issue.ID, RefCommentID: c4.ID}).(*Comment) + r4 := unittest.AssertExistsAndLoadBean(t, &issues_model.Comment{IssueID: i3.ID, RefIssueID: pr.Issue.ID, RefCommentID: c4.ID}).(*issues_model.Comment) refs, err := pr.ResolveCrossReferences(db.DefaultContext) assert.NoError(t, err) @@ -126,13 +127,13 @@ func TestXRef_ResolveCrossReferences(t *testing.T) { assert.Equal(t, r4.ID, refs[2].ID, "bad ref r4: %+v", refs[2]) } -func testCreateIssue(t *testing.T, repo, doer int64, title, content string, ispull bool) *Issue { +func testCreateIssue(t *testing.T, repo, doer int64, title, content string, ispull bool) *issues_model.Issue { r := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: repo}).(*repo_model.Repository) d := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: doer}).(*user_model.User) idx, err := db.GetNextResourceIndex("issue_index", r.ID) assert.NoError(t, err) - i := &Issue{ + i := &issues_model.Issue{ RepoID: r.ID, PosterID: d.ID, Poster: d, @@ -145,39 +146,39 @@ func testCreateIssue(t *testing.T, repo, doer int64, title, content string, ispu ctx, committer, err := db.TxContext() assert.NoError(t, err) defer committer.Close() - err = newIssue(ctx, d, NewIssueOptions{ + err = issues_model.NewIssueWithIndex(ctx, d, issues_model.NewIssueOptions{ Repo: r, Issue: i, }) assert.NoError(t, err) - i, err = getIssueByID(ctx, i.ID) + i, err = issues_model.GetIssueByID(ctx, i.ID) assert.NoError(t, err) - assert.NoError(t, i.addCrossReferences(ctx, d, false)) + assert.NoError(t, i.AddCrossReferences(ctx, d, false)) assert.NoError(t, committer.Commit()) return i } -func testCreatePR(t *testing.T, repo, doer int64, title, content string) *PullRequest { +func testCreatePR(t *testing.T, repo, doer int64, title, content string) *issues_model.PullRequest { r := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: repo}).(*repo_model.Repository) d := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: doer}).(*user_model.User) - i := &Issue{RepoID: r.ID, PosterID: d.ID, Poster: d, Title: title, Content: content, IsPull: true} - pr := &PullRequest{HeadRepoID: repo, BaseRepoID: repo, HeadBranch: "head", BaseBranch: "base", Status: PullRequestStatusMergeable} - assert.NoError(t, NewPullRequest(db.DefaultContext, r, i, nil, nil, pr)) + i := &issues_model.Issue{RepoID: r.ID, PosterID: d.ID, Poster: d, Title: title, Content: content, IsPull: true} + pr := &issues_model.PullRequest{HeadRepoID: repo, BaseRepoID: repo, HeadBranch: "head", BaseBranch: "base", Status: issues_model.PullRequestStatusMergeable} + assert.NoError(t, issues_model.NewPullRequest(db.DefaultContext, r, i, nil, nil, pr)) pr.Issue = i return pr } -func testCreateComment(t *testing.T, repo, doer, issue int64, content string) *Comment { +func testCreateComment(t *testing.T, repo, doer, issue int64, content string) *issues_model.Comment { d := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: doer}).(*user_model.User) - i := unittest.AssertExistsAndLoadBean(t, &Issue{ID: issue}).(*Issue) - c := &Comment{Type: CommentTypeComment, PosterID: doer, Poster: d, IssueID: issue, Issue: i, Content: content} + i := unittest.AssertExistsAndLoadBean(t, &issues_model.Issue{ID: issue}).(*issues_model.Issue) + c := &issues_model.Comment{Type: issues_model.CommentTypeComment, PosterID: doer, Poster: d, IssueID: issue, Issue: i, Content: content} ctx, committer, err := db.TxContext() assert.NoError(t, err) defer committer.Close() err = db.Insert(ctx, c) assert.NoError(t, err) - assert.NoError(t, c.addCrossReferences(ctx, d, false)) + assert.NoError(t, c.AddCrossReferences(ctx, d, false)) assert.NoError(t, committer.Commit()) return c } diff --git a/models/issue_label.go b/models/issues/label.go index 48a48dbb7c..98e2e43961 100644 --- a/models/issue_label.go +++ b/models/issues/label.go @@ -3,7 +3,7 @@ // Use of this source code is governed by a MIT-style // license that can be found in the LICENSE file. -package models +package issues import ( "context" @@ -21,6 +21,53 @@ import ( "xorm.io/builder" ) +// ErrRepoLabelNotExist represents a "RepoLabelNotExist" kind of error. +type ErrRepoLabelNotExist struct { + LabelID int64 + RepoID int64 +} + +// IsErrRepoLabelNotExist checks if an error is a RepoErrLabelNotExist. +func IsErrRepoLabelNotExist(err error) bool { + _, ok := err.(ErrRepoLabelNotExist) + return ok +} + +func (err ErrRepoLabelNotExist) Error() string { + return fmt.Sprintf("label does not exist [label_id: %d, repo_id: %d]", err.LabelID, err.RepoID) +} + +// ErrOrgLabelNotExist represents a "OrgLabelNotExist" kind of error. +type ErrOrgLabelNotExist struct { + LabelID int64 + OrgID int64 +} + +// IsErrOrgLabelNotExist checks if an error is a OrgErrLabelNotExist. +func IsErrOrgLabelNotExist(err error) bool { + _, ok := err.(ErrOrgLabelNotExist) + return ok +} + +func (err ErrOrgLabelNotExist) Error() string { + return fmt.Sprintf("label does not exist [label_id: %d, org_id: %d]", err.LabelID, err.OrgID) +} + +// ErrLabelNotExist represents a "LabelNotExist" kind of error. +type ErrLabelNotExist struct { + LabelID int64 +} + +// IsErrLabelNotExist checks if an error is a ErrLabelNotExist. +func IsErrLabelNotExist(err error) bool { + _, ok := err.(ErrLabelNotExist) + return ok +} + +func (err ErrLabelNotExist) Error() string { + return fmt.Sprintf("label does not exist [label_id: %d]", err.LabelID) +} + // LabelColorPattern is a regexp witch can validate LabelColor var LabelColorPattern = regexp.MustCompile("^#?(?:[0-9a-fA-F]{6}|[0-9a-fA-F]{3})$") @@ -671,7 +718,8 @@ func DeleteIssueLabel(ctx context.Context, issue *Issue, label *Label, doer *use return issue.LoadLabels(ctx) } -func deleteLabelsByRepoID(ctx context.Context, repoID int64) error { +// DeleteLabelsByRepoID deletes labels of some repository +func DeleteLabelsByRepoID(ctx context.Context, repoID int64) error { deleteCond := builder.Select("id").From("label").Where(builder.Eq{"label.repo_id": repoID}) if _, err := db.GetEngine(ctx).In("label_id", deleteCond). @@ -682,3 +730,107 @@ func deleteLabelsByRepoID(ctx context.Context, repoID int64) error { _, err := db.DeleteByBean(ctx, &Label{RepoID: repoID}) return err } + +// CountOrphanedLabels return count of labels witch are broken and not accessible via ui anymore +func CountOrphanedLabels() (int64, error) { + noref, err := db.GetEngine(db.DefaultContext).Table("label").Where("repo_id=? AND org_id=?", 0, 0).Count("label.id") + if err != nil { + return 0, err + } + + norepo, err := db.GetEngine(db.DefaultContext).Table("label"). + Where(builder.And( + builder.Gt{"repo_id": 0}, + builder.NotIn("repo_id", builder.Select("id").From("repository")), + )). + Count() + if err != nil { + return 0, err + } + + noorg, err := db.GetEngine(db.DefaultContext).Table("label"). + Where(builder.And( + builder.Gt{"org_id": 0}, + builder.NotIn("org_id", builder.Select("id").From("user")), + )). + Count() + if err != nil { + return 0, err + } + + return noref + norepo + noorg, nil +} + +// DeleteOrphanedLabels delete labels witch are broken and not accessible via ui anymore +func DeleteOrphanedLabels() error { + // delete labels with no reference + if _, err := db.GetEngine(db.DefaultContext).Table("label").Where("repo_id=? AND org_id=?", 0, 0).Delete(new(Label)); err != nil { + return err + } + + // delete labels with none existing repos + if _, err := db.GetEngine(db.DefaultContext). + Where(builder.And( + builder.Gt{"repo_id": 0}, + builder.NotIn("repo_id", builder.Select("id").From("repository")), + )). + Delete(Label{}); err != nil { + return err + } + + // delete labels with none existing orgs + if _, err := db.GetEngine(db.DefaultContext). + Where(builder.And( + builder.Gt{"org_id": 0}, + builder.NotIn("org_id", builder.Select("id").From("user")), + )). + Delete(Label{}); err != nil { + return err + } + + return nil +} + +// CountOrphanedIssueLabels return count of IssueLabels witch have no label behind anymore +func CountOrphanedIssueLabels() (int64, error) { + return db.GetEngine(db.DefaultContext).Table("issue_label"). + NotIn("label_id", builder.Select("id").From("label")). + Count() +} + +// DeleteOrphanedIssueLabels delete IssueLabels witch have no label behind anymore +func DeleteOrphanedIssueLabels() error { + _, err := db.GetEngine(db.DefaultContext). + NotIn("label_id", builder.Select("id").From("label")). + Delete(IssueLabel{}) + return err +} + +// CountIssueLabelWithOutsideLabels count label comments with outside label +func CountIssueLabelWithOutsideLabels() (int64, error) { + return db.GetEngine(db.DefaultContext).Where(builder.Expr("(label.org_id = 0 AND issue.repo_id != label.repo_id) OR (label.repo_id = 0 AND label.org_id != repository.owner_id)")). + Table("issue_label"). + Join("inner", "label", "issue_label.label_id = label.id "). + Join("inner", "issue", "issue.id = issue_label.issue_id "). + Join("inner", "repository", "issue.repo_id = repository.id"). + Count(new(IssueLabel)) +} + +// FixIssueLabelWithOutsideLabels fix label comments with outside label +func FixIssueLabelWithOutsideLabels() (int64, error) { + res, err := db.GetEngine(db.DefaultContext).Exec(`DELETE FROM issue_label WHERE issue_label.id IN ( + SELECT il_too.id FROM ( + SELECT il_too_too.id + FROM issue_label AS il_too_too + INNER JOIN label ON il_too_too.label_id = label.id + INNER JOIN issue on issue.id = il_too_too.issue_id + INNER JOIN repository on repository.id = issue.repo_id + WHERE + (label.org_id = 0 AND issue.repo_id != label.repo_id) OR (label.repo_id = 0 AND label.org_id != repository.owner_id) + ) AS il_too )`) + if err != nil { + return 0, err + } + + return res.RowsAffected() +} diff --git a/models/issues/label_test.go b/models/issues/label_test.go new file mode 100644 index 0000000000..33f114b5fe --- /dev/null +++ b/models/issues/label_test.go @@ -0,0 +1,395 @@ +// Copyright 2017 The Gitea Authors. All rights reserved. +// Use of this source code is governed by a MIT-style +// license that can be found in the LICENSE file. + +package issues_test + +import ( + "html/template" + "testing" + + "code.gitea.io/gitea/models/db" + issues_model "code.gitea.io/gitea/models/issues" + repo_model "code.gitea.io/gitea/models/repo" + "code.gitea.io/gitea/models/unittest" + user_model "code.gitea.io/gitea/models/user" + + "github.com/stretchr/testify/assert" +) + +// TODO TestGetLabelTemplateFile + +func TestLabel_CalOpenIssues(t *testing.T) { + assert.NoError(t, unittest.PrepareTestDatabase()) + label := unittest.AssertExistsAndLoadBean(t, &issues_model.Label{ID: 1}).(*issues_model.Label) + label.CalOpenIssues() + assert.EqualValues(t, 2, label.NumOpenIssues) +} + +func TestLabel_ForegroundColor(t *testing.T) { + assert.NoError(t, unittest.PrepareTestDatabase()) + label := unittest.AssertExistsAndLoadBean(t, &issues_model.Label{ID: 1}).(*issues_model.Label) + assert.Equal(t, template.CSS("#000"), label.ForegroundColor()) + + label = unittest.AssertExistsAndLoadBean(t, &issues_model.Label{ID: 2}).(*issues_model.Label) + assert.Equal(t, template.CSS("#fff"), label.ForegroundColor()) +} + +func TestNewLabels(t *testing.T) { + assert.NoError(t, unittest.PrepareTestDatabase()) + labels := []*issues_model.Label{ + {RepoID: 2, Name: "labelName2", Color: "#123456"}, + {RepoID: 3, Name: "labelName3", Color: "#123"}, + {RepoID: 4, Name: "labelName4", Color: "ABCDEF"}, + {RepoID: 5, Name: "labelName5", Color: "DEF"}, + } + assert.Error(t, issues_model.NewLabel(db.DefaultContext, &issues_model.Label{RepoID: 3, Name: "invalid Color", Color: ""})) + assert.Error(t, issues_model.NewLabel(db.DefaultContext, &issues_model.Label{RepoID: 3, Name: "invalid Color", Color: "#45G"})) + assert.Error(t, issues_model.NewLabel(db.DefaultContext, &issues_model.Label{RepoID: 3, Name: "invalid Color", Color: "#12345G"})) + assert.Error(t, issues_model.NewLabel(db.DefaultContext, &issues_model.Label{RepoID: 3, Name: "invalid Color", Color: "45G"})) + assert.Error(t, issues_model.NewLabel(db.DefaultContext, &issues_model.Label{RepoID: 3, Name: "invalid Color", Color: "12345G"})) + for _, label := range labels { + unittest.AssertNotExistsBean(t, label) + } + assert.NoError(t, issues_model.NewLabels(labels...)) + for _, label := range labels { + unittest.AssertExistsAndLoadBean(t, label, unittest.Cond("id = ?", label.ID)) + } + unittest.CheckConsistencyFor(t, &issues_model.Label{}, &repo_model.Repository{}) +} + +func TestGetLabelByID(t *testing.T) { + assert.NoError(t, unittest.PrepareTestDatabase()) + label, err := issues_model.GetLabelByID(db.DefaultContext, 1) + assert.NoError(t, err) + assert.EqualValues(t, 1, label.ID) + + _, err = issues_model.GetLabelByID(db.DefaultContext, unittest.NonexistentID) + assert.True(t, issues_model.IsErrLabelNotExist(err)) +} + +func TestGetLabelInRepoByName(t *testing.T) { + assert.NoError(t, unittest.PrepareTestDatabase()) + label, err := issues_model.GetLabelInRepoByName(db.DefaultContext, 1, "label1") + assert.NoError(t, err) + assert.EqualValues(t, 1, label.ID) + assert.Equal(t, "label1", label.Name) + + _, err = issues_model.GetLabelInRepoByName(db.DefaultContext, 1, "") + assert.True(t, issues_model.IsErrRepoLabelNotExist(err)) + + _, err = issues_model.GetLabelInRepoByName(db.DefaultContext, unittest.NonexistentID, "nonexistent") + assert.True(t, issues_model.IsErrRepoLabelNotExist(err)) +} + +func TestGetLabelInRepoByNames(t *testing.T) { + assert.NoError(t, unittest.PrepareTestDatabase()) + labelIDs, err := issues_model.GetLabelIDsInRepoByNames(1, []string{"label1", "label2"}) + assert.NoError(t, err) + + assert.Len(t, labelIDs, 2) + + assert.Equal(t, int64(1), labelIDs[0]) + assert.Equal(t, int64(2), labelIDs[1]) +} + +func TestGetLabelInRepoByNamesDiscardsNonExistentLabels(t *testing.T) { + assert.NoError(t, unittest.PrepareTestDatabase()) + // label3 doesn't exists.. See labels.yml + labelIDs, err := issues_model.GetLabelIDsInRepoByNames(1, []string{"label1", "label2", "label3"}) + assert.NoError(t, err) + + assert.Len(t, labelIDs, 2) + + assert.Equal(t, int64(1), labelIDs[0]) + assert.Equal(t, int64(2), labelIDs[1]) + assert.NoError(t, err) +} + +func TestGetLabelInRepoByID(t *testing.T) { + assert.NoError(t, unittest.PrepareTestDatabase()) + label, err := issues_model.GetLabelInRepoByID(db.DefaultContext, 1, 1) + assert.NoError(t, err) + assert.EqualValues(t, 1, label.ID) + + _, err = issues_model.GetLabelInRepoByID(db.DefaultContext, 1, -1) + assert.True(t, issues_model.IsErrRepoLabelNotExist(err)) + + _, err = issues_model.GetLabelInRepoByID(db.DefaultContext, unittest.NonexistentID, unittest.NonexistentID) + assert.True(t, issues_model.IsErrRepoLabelNotExist(err)) +} + +func TestGetLabelsInRepoByIDs(t *testing.T) { + assert.NoError(t, unittest.PrepareTestDatabase()) + labels, err := issues_model.GetLabelsInRepoByIDs(1, []int64{1, 2, unittest.NonexistentID}) + assert.NoError(t, err) + if assert.Len(t, labels, 2) { + assert.EqualValues(t, 1, labels[0].ID) + assert.EqualValues(t, 2, labels[1].ID) + } +} + +func TestGetLabelsByRepoID(t *testing.T) { + assert.NoError(t, unittest.PrepareTestDatabase()) + testSuccess := func(repoID int64, sortType string, expectedIssueIDs []int64) { + labels, err := issues_model.GetLabelsByRepoID(db.DefaultContext, repoID, sortType, db.ListOptions{}) + assert.NoError(t, err) + assert.Len(t, labels, len(expectedIssueIDs)) + for i, label := range labels { + assert.EqualValues(t, expectedIssueIDs[i], label.ID) + } + } + testSuccess(1, "leastissues", []int64{2, 1}) + testSuccess(1, "mostissues", []int64{1, 2}) + testSuccess(1, "reversealphabetically", []int64{2, 1}) + testSuccess(1, "default", []int64{1, 2}) +} + +// Org versions + +func TestGetLabelInOrgByName(t *testing.T) { + assert.NoError(t, unittest.PrepareTestDatabase()) + label, err := issues_model.GetLabelInOrgByName(db.DefaultContext, 3, "orglabel3") + assert.NoError(t, err) + assert.EqualValues(t, 3, label.ID) + assert.Equal(t, "orglabel3", label.Name) + + _, err = issues_model.GetLabelInOrgByName(db.DefaultContext, 3, "") + assert.True(t, issues_model.IsErrOrgLabelNotExist(err)) + + _, err = issues_model.GetLabelInOrgByName(db.DefaultContext, 0, "orglabel3") + assert.True(t, issues_model.IsErrOrgLabelNotExist(err)) + + _, err = issues_model.GetLabelInOrgByName(db.DefaultContext, -1, "orglabel3") + assert.True(t, issues_model.IsErrOrgLabelNotExist(err)) + + _, err = issues_model.GetLabelInOrgByName(db.DefaultContext, unittest.NonexistentID, "nonexistent") + assert.True(t, issues_model.IsErrOrgLabelNotExist(err)) +} + +func TestGetLabelInOrgByNames(t *testing.T) { + assert.NoError(t, unittest.PrepareTestDatabase()) + labelIDs, err := issues_model.GetLabelIDsInOrgByNames(3, []string{"orglabel3", "orglabel4"}) + assert.NoError(t, err) + + assert.Len(t, labelIDs, 2) + + assert.Equal(t, int64(3), labelIDs[0]) + assert.Equal(t, int64(4), labelIDs[1]) +} + +func TestGetLabelInOrgByNamesDiscardsNonExistentLabels(t *testing.T) { + assert.NoError(t, unittest.PrepareTestDatabase()) + // orglabel99 doesn't exists.. See labels.yml + labelIDs, err := issues_model.GetLabelIDsInOrgByNames(3, []string{"orglabel3", "orglabel4", "orglabel99"}) + assert.NoError(t, err) + + assert.Len(t, labelIDs, 2) + + assert.Equal(t, int64(3), labelIDs[0]) + assert.Equal(t, int64(4), labelIDs[1]) + assert.NoError(t, err) +} + +func TestGetLabelInOrgByID(t *testing.T) { + assert.NoError(t, unittest.PrepareTestDatabase()) + label, err := issues_model.GetLabelInOrgByID(db.DefaultContext, 3, 3) + assert.NoError(t, err) + assert.EqualValues(t, 3, label.ID) + + _, err = issues_model.GetLabelInOrgByID(db.DefaultContext, 3, -1) + assert.True(t, issues_model.IsErrOrgLabelNotExist(err)) + + _, err = issues_model.GetLabelInOrgByID(db.DefaultContext, 0, 3) + assert.True(t, issues_model.IsErrOrgLabelNotExist(err)) + + _, err = issues_model.GetLabelInOrgByID(db.DefaultContext, -1, 3) + assert.True(t, issues_model.IsErrOrgLabelNotExist(err)) + + _, err = issues_model.GetLabelInOrgByID(db.DefaultContext, unittest.NonexistentID, unittest.NonexistentID) + assert.True(t, issues_model.IsErrOrgLabelNotExist(err)) +} + +func TestGetLabelsInOrgByIDs(t *testing.T) { + assert.NoError(t, unittest.PrepareTestDatabase()) + labels, err := issues_model.GetLabelsInOrgByIDs(3, []int64{3, 4, unittest.NonexistentID}) + assert.NoError(t, err) + if assert.Len(t, labels, 2) { + assert.EqualValues(t, 3, labels[0].ID) + assert.EqualValues(t, 4, labels[1].ID) + } +} + +func TestGetLabelsByOrgID(t *testing.T) { + assert.NoError(t, unittest.PrepareTestDatabase()) + testSuccess := func(orgID int64, sortType string, expectedIssueIDs []int64) { + labels, err := issues_model.GetLabelsByOrgID(db.DefaultContext, orgID, sortType, db.ListOptions{}) + assert.NoError(t, err) + assert.Len(t, labels, len(expectedIssueIDs)) + for i, label := range labels { + assert.EqualValues(t, expectedIssueIDs[i], label.ID) + } + } + testSuccess(3, "leastissues", []int64{3, 4}) + testSuccess(3, "mostissues", []int64{4, 3}) + testSuccess(3, "reversealphabetically", []int64{4, 3}) + testSuccess(3, "default", []int64{3, 4}) + + var err error + _, err = issues_model.GetLabelsByOrgID(db.DefaultContext, 0, "leastissues", db.ListOptions{}) + assert.True(t, issues_model.IsErrOrgLabelNotExist(err)) + + _, err = issues_model.GetLabelsByOrgID(db.DefaultContext, -1, "leastissues", db.ListOptions{}) + assert.True(t, issues_model.IsErrOrgLabelNotExist(err)) +} + +// + +func TestGetLabelsByIssueID(t *testing.T) { + assert.NoError(t, unittest.PrepareTestDatabase()) + labels, err := issues_model.GetLabelsByIssueID(db.DefaultContext, 1) + assert.NoError(t, err) + if assert.Len(t, labels, 1) { + assert.EqualValues(t, 1, labels[0].ID) + } + + labels, err = issues_model.GetLabelsByIssueID(db.DefaultContext, unittest.NonexistentID) + assert.NoError(t, err) + assert.Len(t, labels, 0) +} + +func TestUpdateLabel(t *testing.T) { + assert.NoError(t, unittest.PrepareTestDatabase()) + label := unittest.AssertExistsAndLoadBean(t, &issues_model.Label{ID: 1}).(*issues_model.Label) + // make sure update wont overwrite it + update := &issues_model.Label{ + ID: label.ID, + Color: "#ffff00", + Name: "newLabelName", + Description: label.Description, + } + label.Color = update.Color + label.Name = update.Name + assert.NoError(t, issues_model.UpdateLabel(update)) + newLabel := unittest.AssertExistsAndLoadBean(t, &issues_model.Label{ID: 1}).(*issues_model.Label) + 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) + unittest.CheckConsistencyFor(t, &issues_model.Label{}, &repo_model.Repository{}) +} + +func TestDeleteLabel(t *testing.T) { + assert.NoError(t, unittest.PrepareTestDatabase()) + label := unittest.AssertExistsAndLoadBean(t, &issues_model.Label{ID: 1}).(*issues_model.Label) + assert.NoError(t, issues_model.DeleteLabel(label.RepoID, label.ID)) + unittest.AssertNotExistsBean(t, &issues_model.Label{ID: label.ID, RepoID: label.RepoID}) + + assert.NoError(t, issues_model.DeleteLabel(label.RepoID, label.ID)) + unittest.AssertNotExistsBean(t, &issues_model.Label{ID: label.ID}) + + assert.NoError(t, issues_model.DeleteLabel(unittest.NonexistentID, unittest.NonexistentID)) + unittest.CheckConsistencyFor(t, &issues_model.Label{}, &repo_model.Repository{}) +} + +func TestHasIssueLabel(t *testing.T) { + assert.NoError(t, unittest.PrepareTestDatabase()) + assert.True(t, issues_model.HasIssueLabel(db.DefaultContext, 1, 1)) + assert.False(t, issues_model.HasIssueLabel(db.DefaultContext, 1, 2)) + assert.False(t, issues_model.HasIssueLabel(db.DefaultContext, unittest.NonexistentID, unittest.NonexistentID)) +} + +func TestNewIssueLabel(t *testing.T) { + assert.NoError(t, unittest.PrepareTestDatabase()) + label := unittest.AssertExistsAndLoadBean(t, &issues_model.Label{ID: 2}).(*issues_model.Label) + issue := unittest.AssertExistsAndLoadBean(t, &issues_model.Issue{ID: 1}).(*issues_model.Issue) + doer := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2}).(*user_model.User) + + // add new IssueLabel + prevNumIssues := label.NumIssues + assert.NoError(t, issues_model.NewIssueLabel(issue, label, doer)) + unittest.AssertExistsAndLoadBean(t, &issues_model.IssueLabel{IssueID: issue.ID, LabelID: label.ID}) + unittest.AssertExistsAndLoadBean(t, &issues_model.Comment{ + Type: issues_model.CommentTypeLabel, + PosterID: doer.ID, + IssueID: issue.ID, + LabelID: label.ID, + Content: "1", + }) + label = unittest.AssertExistsAndLoadBean(t, &issues_model.Label{ID: 2}).(*issues_model.Label) + assert.EqualValues(t, prevNumIssues+1, label.NumIssues) + + // re-add existing IssueLabel + assert.NoError(t, issues_model.NewIssueLabel(issue, label, doer)) + unittest.CheckConsistencyFor(t, &issues_model.Issue{}, &issues_model.Label{}) +} + +func TestNewIssueLabels(t *testing.T) { + assert.NoError(t, unittest.PrepareTestDatabase()) + label1 := unittest.AssertExistsAndLoadBean(t, &issues_model.Label{ID: 1}).(*issues_model.Label) + label2 := unittest.AssertExistsAndLoadBean(t, &issues_model.Label{ID: 2}).(*issues_model.Label) + issue := unittest.AssertExistsAndLoadBean(t, &issues_model.Issue{ID: 5}).(*issues_model.Issue) + doer := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2}).(*user_model.User) + + assert.NoError(t, issues_model.NewIssueLabels(issue, []*issues_model.Label{label1, label2}, doer)) + unittest.AssertExistsAndLoadBean(t, &issues_model.IssueLabel{IssueID: issue.ID, LabelID: label1.ID}) + unittest.AssertExistsAndLoadBean(t, &issues_model.Comment{ + Type: issues_model.CommentTypeLabel, + PosterID: doer.ID, + IssueID: issue.ID, + LabelID: label1.ID, + Content: "1", + }) + unittest.AssertExistsAndLoadBean(t, &issues_model.IssueLabel{IssueID: issue.ID, LabelID: label1.ID}) + label1 = unittest.AssertExistsAndLoadBean(t, &issues_model.Label{ID: 1}).(*issues_model.Label) + assert.EqualValues(t, 3, label1.NumIssues) + assert.EqualValues(t, 1, label1.NumClosedIssues) + label2 = unittest.AssertExistsAndLoadBean(t, &issues_model.Label{ID: 2}).(*issues_model.Label) + assert.EqualValues(t, 1, label2.NumIssues) + assert.EqualValues(t, 1, label2.NumClosedIssues) + + // corner case: test empty slice + assert.NoError(t, issues_model.NewIssueLabels(issue, []*issues_model.Label{}, doer)) + + unittest.CheckConsistencyFor(t, &issues_model.Issue{}, &issues_model.Label{}) +} + +func TestDeleteIssueLabel(t *testing.T) { + assert.NoError(t, unittest.PrepareTestDatabase()) + testSuccess := func(labelID, issueID, doerID int64) { + label := unittest.AssertExistsAndLoadBean(t, &issues_model.Label{ID: labelID}).(*issues_model.Label) + issue := unittest.AssertExistsAndLoadBean(t, &issues_model.Issue{ID: issueID}).(*issues_model.Issue) + doer := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: doerID}).(*user_model.User) + + expectedNumIssues := label.NumIssues + expectedNumClosedIssues := label.NumClosedIssues + if unittest.BeanExists(t, &issues_model.IssueLabel{IssueID: issueID, LabelID: labelID}) { + expectedNumIssues-- + if issue.IsClosed { + expectedNumClosedIssues-- + } + } + + ctx, committer, err := db.TxContext() + defer committer.Close() + assert.NoError(t, err) + assert.NoError(t, issues_model.DeleteIssueLabel(ctx, issue, label, doer)) + assert.NoError(t, committer.Commit()) + + unittest.AssertNotExistsBean(t, &issues_model.IssueLabel{IssueID: issueID, LabelID: labelID}) + unittest.AssertExistsAndLoadBean(t, &issues_model.Comment{ + Type: issues_model.CommentTypeLabel, + PosterID: doerID, + IssueID: issueID, + LabelID: labelID, + }, `content=""`) + label = unittest.AssertExistsAndLoadBean(t, &issues_model.Label{ID: labelID}).(*issues_model.Label) + assert.EqualValues(t, expectedNumIssues, label.NumIssues) + assert.EqualValues(t, expectedNumClosedIssues, label.NumClosedIssues) + } + testSuccess(1, 1, 2) + testSuccess(2, 5, 2) + testSuccess(1, 1, 2) // delete non-existent IssueLabel + + unittest.CheckConsistencyFor(t, &issues_model.Issue{}, &issues_model.Label{}) +} diff --git a/models/issues/main_test.go b/models/issues/main_test.go index 30f6ff02fb..e34bef62ca 100644 --- a/models/issues/main_test.go +++ b/models/issues/main_test.go @@ -2,14 +2,20 @@ // Use of this source code is governed by a MIT-style // license that can be found in the LICENSE file. -package issues +package issues_test import ( "path/filepath" "testing" + _ "code.gitea.io/gitea/models" + issues_model "code.gitea.io/gitea/models/issues" + _ "code.gitea.io/gitea/models/repo" "code.gitea.io/gitea/models/unittest" + _ "code.gitea.io/gitea/models/user" "code.gitea.io/gitea/modules/setting" + + "github.com/stretchr/testify/assert" ) func init() { @@ -17,14 +23,18 @@ func init() { setting.LoadForTest() } +func TestFixturesAreConsistent(t *testing.T) { + assert.NoError(t, unittest.PrepareTestDatabase()) + unittest.CheckConsistencyFor(t, + &issues_model.Issue{}, + &issues_model.PullRequest{}, + &issues_model.Milestone{}, + &issues_model.Label{}, + ) +} + func TestMain(m *testing.M) { unittest.MainTest(m, &unittest.TestOptions{ GiteaRootPath: filepath.Join("..", ".."), - FixtureFiles: []string{ - "reaction.yml", - "user.yml", - "repository.yml", - "milestone.yml", - }, }) } diff --git a/models/issues/milestone.go b/models/issues/milestone.go index f7172f6448..6c10959108 100644 --- a/models/issues/milestone.go +++ b/models/issues/milestone.go @@ -292,11 +292,17 @@ func DeleteMilestoneByRepoID(repoID, id int64) error { return err } - numMilestones, err := countRepoMilestones(ctx, repo.ID) + numMilestones, err := CountMilestones(ctx, GetMilestonesOption{ + RepoID: repo.ID, + State: api.StateAll, + }) if err != nil { return err } - numClosedMilestones, err := countRepoClosedMilestones(ctx, repo.ID) + numClosedMilestones, err := CountMilestones(ctx, GetMilestonesOption{ + RepoID: repo.ID, + State: api.StateClosed, + }) if err != nil { return err } @@ -428,13 +434,6 @@ func GetMilestonesByRepoIDs(repoIDs []int64, page int, isClosed bool, sortType s ) } -// ____ _ _ -// / ___|| |_ __ _| |_ ___ -// \___ \| __/ _` | __/ __| -// ___) | || (_| | |_\__ \ -// |____/ \__\__,_|\__|___/ -// - // MilestonesStats represents milestone statistic information. type MilestonesStats struct { OpenCount, ClosedCount int64 @@ -503,23 +502,13 @@ func GetMilestonesStatsByRepoCondAndKw(repoCond builder.Cond, keyword string) (* return stats, nil } -func countRepoMilestones(ctx context.Context, repoID int64) (int64, error) { - return db.GetEngine(ctx). - Where("repo_id=?", repoID). - Count(new(Milestone)) -} - -func countRepoClosedMilestones(ctx context.Context, repoID int64) (int64, error) { +// CountMilestones returns number of milestones in given repository with other options +func CountMilestones(ctx context.Context, opts GetMilestonesOption) (int64, error) { return db.GetEngine(ctx). - Where("repo_id=? AND is_closed=?", repoID, true). + Where(opts.toCond()). Count(new(Milestone)) } -// CountRepoClosedMilestones returns number of closed milestones in given repository. -func CountRepoClosedMilestones(repoID int64) (int64, error) { - return countRepoClosedMilestones(db.DefaultContext, repoID) -} - // CountMilestonesByRepoCond map from repo conditions to number of milestones matching the options` func CountMilestonesByRepoCond(repoCond builder.Cond, isClosed bool) (map[int64]int64, error) { sess := db.GetEngine(db.DefaultContext).Where("is_closed = ?", isClosed) diff --git a/models/issues/milestone_test.go b/models/issues/milestone_test.go index e087318320..a6fbf9c23b 100644 --- a/models/issues/milestone_test.go +++ b/models/issues/milestone_test.go @@ -2,44 +2,46 @@ // Use of this source code is governed by a MIT-style // license that can be found in the LICENSE file. -package issues +package issues_test import ( "sort" "testing" "code.gitea.io/gitea/models/db" + issues_model "code.gitea.io/gitea/models/issues" repo_model "code.gitea.io/gitea/models/repo" "code.gitea.io/gitea/models/unittest" "code.gitea.io/gitea/modules/setting" api "code.gitea.io/gitea/modules/structs" + "code.gitea.io/gitea/modules/timeutil" "github.com/stretchr/testify/assert" "xorm.io/builder" ) func TestMilestone_State(t *testing.T) { - assert.Equal(t, api.StateOpen, (&Milestone{IsClosed: false}).State()) - assert.Equal(t, api.StateClosed, (&Milestone{IsClosed: true}).State()) + assert.Equal(t, api.StateOpen, (&issues_model.Milestone{IsClosed: false}).State()) + assert.Equal(t, api.StateClosed, (&issues_model.Milestone{IsClosed: true}).State()) } func TestGetMilestoneByRepoID(t *testing.T) { assert.NoError(t, unittest.PrepareTestDatabase()) - milestone, err := GetMilestoneByRepoID(db.DefaultContext, 1, 1) + milestone, err := issues_model.GetMilestoneByRepoID(db.DefaultContext, 1, 1) assert.NoError(t, err) assert.EqualValues(t, 1, milestone.ID) assert.EqualValues(t, 1, milestone.RepoID) - _, err = GetMilestoneByRepoID(db.DefaultContext, unittest.NonexistentID, unittest.NonexistentID) - assert.True(t, IsErrMilestoneNotExist(err)) + _, err = issues_model.GetMilestoneByRepoID(db.DefaultContext, unittest.NonexistentID, unittest.NonexistentID) + assert.True(t, issues_model.IsErrMilestoneNotExist(err)) } func TestGetMilestonesByRepoID(t *testing.T) { assert.NoError(t, unittest.PrepareTestDatabase()) test := func(repoID int64, state api.StateType) { repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: repoID}).(*repo_model.Repository) - milestones, _, err := GetMilestones(GetMilestonesOption{ + milestones, _, err := issues_model.GetMilestones(issues_model.GetMilestonesOption{ RepoID: repo.ID, State: state, }) @@ -76,7 +78,7 @@ func TestGetMilestonesByRepoID(t *testing.T) { test(3, api.StateClosed) test(3, api.StateAll) - milestones, _, err := GetMilestones(GetMilestonesOption{ + milestones, _, err := issues_model.GetMilestones(issues_model.GetMilestonesOption{ RepoID: unittest.NonexistentID, State: api.StateOpen, }) @@ -87,9 +89,9 @@ func TestGetMilestonesByRepoID(t *testing.T) { func TestGetMilestones(t *testing.T) { assert.NoError(t, unittest.PrepareTestDatabase()) repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 1}).(*repo_model.Repository) - test := func(sortType string, sortCond func(*Milestone) int) { + test := func(sortType string, sortCond func(*issues_model.Milestone) int) { for _, page := range []int{0, 1} { - milestones, _, err := GetMilestones(GetMilestonesOption{ + milestones, _, err := issues_model.GetMilestones(issues_model.GetMilestonesOption{ ListOptions: db.ListOptions{ Page: page, PageSize: setting.UI.IssuePagingNum, @@ -106,7 +108,7 @@ func TestGetMilestones(t *testing.T) { } assert.True(t, sort.IntsAreSorted(values)) - milestones, _, err = GetMilestones(GetMilestonesOption{ + milestones, _, err = issues_model.GetMilestones(issues_model.GetMilestonesOption{ ListOptions: db.ListOptions{ Page: page, PageSize: setting.UI.IssuePagingNum, @@ -125,22 +127,22 @@ func TestGetMilestones(t *testing.T) { assert.True(t, sort.IntsAreSorted(values)) } } - test("furthestduedate", func(milestone *Milestone) int { + test("furthestduedate", func(milestone *issues_model.Milestone) int { return -int(milestone.DeadlineUnix) }) - test("leastcomplete", func(milestone *Milestone) int { + test("leastcomplete", func(milestone *issues_model.Milestone) int { return milestone.Completeness }) - test("mostcomplete", func(milestone *Milestone) int { + test("mostcomplete", func(milestone *issues_model.Milestone) int { return -milestone.Completeness }) - test("leastissues", func(milestone *Milestone) int { + test("leastissues", func(milestone *issues_model.Milestone) int { return milestone.NumIssues }) - test("mostissues", func(milestone *Milestone) int { + test("mostissues", func(milestone *issues_model.Milestone) int { return -milestone.NumIssues }) - test("soonestduedate", func(milestone *Milestone) int { + test("soonestduedate", func(milestone *issues_model.Milestone) int { return int(milestone.DeadlineUnix) }) } @@ -149,7 +151,10 @@ func TestCountRepoMilestones(t *testing.T) { assert.NoError(t, unittest.PrepareTestDatabase()) test := func(repoID int64) { repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: repoID}).(*repo_model.Repository) - count, err := countRepoMilestones(db.DefaultContext, repoID) + count, err := issues_model.CountMilestones(db.DefaultContext, issues_model.GetMilestonesOption{ + RepoID: repoID, + State: api.StateAll, + }) assert.NoError(t, err) assert.EqualValues(t, repo.NumMilestones, count) } @@ -157,7 +162,10 @@ func TestCountRepoMilestones(t *testing.T) { test(2) test(3) - count, err := countRepoMilestones(db.DefaultContext, unittest.NonexistentID) + count, err := issues_model.CountMilestones(db.DefaultContext, issues_model.GetMilestonesOption{ + RepoID: unittest.NonexistentID, + State: api.StateAll, + }) assert.NoError(t, err) assert.EqualValues(t, 0, count) } @@ -166,7 +174,10 @@ func TestCountRepoClosedMilestones(t *testing.T) { assert.NoError(t, unittest.PrepareTestDatabase()) test := func(repoID int64) { repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: repoID}).(*repo_model.Repository) - count, err := CountRepoClosedMilestones(repoID) + count, err := issues_model.CountMilestones(db.DefaultContext, issues_model.GetMilestonesOption{ + RepoID: repoID, + State: api.StateClosed, + }) assert.NoError(t, err) assert.EqualValues(t, repo.NumClosedMilestones, count) } @@ -174,7 +185,10 @@ func TestCountRepoClosedMilestones(t *testing.T) { test(2) test(3) - count, err := CountRepoClosedMilestones(unittest.NonexistentID) + count, err := issues_model.CountMilestones(db.DefaultContext, issues_model.GetMilestonesOption{ + RepoID: unittest.NonexistentID, + State: api.StateClosed, + }) assert.NoError(t, err) assert.EqualValues(t, 0, count) } @@ -188,12 +202,12 @@ func TestCountMilestonesByRepoIDs(t *testing.T) { repo1OpenCount, repo1ClosedCount := milestonesCount(1) repo2OpenCount, repo2ClosedCount := milestonesCount(2) - openCounts, err := CountMilestonesByRepoCond(builder.In("repo_id", []int64{1, 2}), false) + openCounts, err := issues_model.CountMilestonesByRepoCond(builder.In("repo_id", []int64{1, 2}), false) assert.NoError(t, err) assert.EqualValues(t, repo1OpenCount, openCounts[1]) assert.EqualValues(t, repo2OpenCount, openCounts[2]) - closedCounts, err := CountMilestonesByRepoCond(builder.In("repo_id", []int64{1, 2}), true) + closedCounts, err := issues_model.CountMilestonesByRepoCond(builder.In("repo_id", []int64{1, 2}), true) assert.NoError(t, err) assert.EqualValues(t, repo1ClosedCount, closedCounts[1]) assert.EqualValues(t, repo2ClosedCount, closedCounts[2]) @@ -203,9 +217,9 @@ func TestGetMilestonesByRepoIDs(t *testing.T) { assert.NoError(t, unittest.PrepareTestDatabase()) repo1 := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 1}).(*repo_model.Repository) repo2 := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 2}).(*repo_model.Repository) - test := func(sortType string, sortCond func(*Milestone) int) { + test := func(sortType string, sortCond func(*issues_model.Milestone) int) { for _, page := range []int{0, 1} { - openMilestones, err := GetMilestonesByRepoIDs([]int64{repo1.ID, repo2.ID}, page, false, sortType) + openMilestones, err := issues_model.GetMilestonesByRepoIDs([]int64{repo1.ID, repo2.ID}, page, false, sortType) assert.NoError(t, err) assert.Len(t, openMilestones, repo1.NumOpenMilestones+repo2.NumOpenMilestones) values := make([]int, len(openMilestones)) @@ -214,7 +228,7 @@ func TestGetMilestonesByRepoIDs(t *testing.T) { } assert.True(t, sort.IntsAreSorted(values)) - closedMilestones, err := GetMilestonesByRepoIDs([]int64{repo1.ID, repo2.ID}, page, true, sortType) + closedMilestones, err := issues_model.GetMilestonesByRepoIDs([]int64{repo1.ID, repo2.ID}, page, true, sortType) assert.NoError(t, err) assert.Len(t, closedMilestones, repo1.NumClosedMilestones+repo2.NumClosedMilestones) values = make([]int, len(closedMilestones)) @@ -224,22 +238,22 @@ func TestGetMilestonesByRepoIDs(t *testing.T) { assert.True(t, sort.IntsAreSorted(values)) } } - test("furthestduedate", func(milestone *Milestone) int { + test("furthestduedate", func(milestone *issues_model.Milestone) int { return -int(milestone.DeadlineUnix) }) - test("leastcomplete", func(milestone *Milestone) int { + test("leastcomplete", func(milestone *issues_model.Milestone) int { return milestone.Completeness }) - test("mostcomplete", func(milestone *Milestone) int { + test("mostcomplete", func(milestone *issues_model.Milestone) int { return -milestone.Completeness }) - test("leastissues", func(milestone *Milestone) int { + test("leastissues", func(milestone *issues_model.Milestone) int { return milestone.NumIssues }) - test("mostissues", func(milestone *Milestone) int { + test("mostissues", func(milestone *issues_model.Milestone) int { return -milestone.NumIssues }) - test("soonestduedate", func(milestone *Milestone) int { + test("soonestduedate", func(milestone *issues_model.Milestone) int { return int(milestone.DeadlineUnix) }) } @@ -249,7 +263,7 @@ func TestGetMilestonesStats(t *testing.T) { test := func(repoID int64) { repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: repoID}).(*repo_model.Repository) - stats, err := GetMilestonesStatsByRepoCond(builder.And(builder.Eq{"repo_id": repoID})) + stats, err := issues_model.GetMilestonesStatsByRepoCond(builder.And(builder.Eq{"repo_id": repoID})) assert.NoError(t, err) assert.EqualValues(t, repo.NumMilestones-repo.NumClosedMilestones, stats.OpenCount) assert.EqualValues(t, repo.NumClosedMilestones, stats.ClosedCount) @@ -258,7 +272,7 @@ func TestGetMilestonesStats(t *testing.T) { test(2) test(3) - stats, err := GetMilestonesStatsByRepoCond(builder.And(builder.Eq{"repo_id": unittest.NonexistentID})) + stats, err := issues_model.GetMilestonesStatsByRepoCond(builder.And(builder.Eq{"repo_id": unittest.NonexistentID})) assert.NoError(t, err) assert.EqualValues(t, 0, stats.OpenCount) assert.EqualValues(t, 0, stats.ClosedCount) @@ -266,8 +280,75 @@ func TestGetMilestonesStats(t *testing.T) { repo1 := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 1}).(*repo_model.Repository) repo2 := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 2}).(*repo_model.Repository) - milestoneStats, err := GetMilestonesStatsByRepoCond(builder.In("repo_id", []int64{repo1.ID, repo2.ID})) + milestoneStats, err := issues_model.GetMilestonesStatsByRepoCond(builder.In("repo_id", []int64{repo1.ID, repo2.ID})) assert.NoError(t, err) assert.EqualValues(t, repo1.NumOpenMilestones+repo2.NumOpenMilestones, milestoneStats.OpenCount) assert.EqualValues(t, repo1.NumClosedMilestones+repo2.NumClosedMilestones, milestoneStats.ClosedCount) } + +func TestNewMilestone(t *testing.T) { + assert.NoError(t, unittest.PrepareTestDatabase()) + milestone := &issues_model.Milestone{ + RepoID: 1, + Name: "milestoneName", + Content: "milestoneContent", + } + + assert.NoError(t, issues_model.NewMilestone(milestone)) + unittest.AssertExistsAndLoadBean(t, milestone) + unittest.CheckConsistencyFor(t, &repo_model.Repository{ID: milestone.RepoID}, &issues_model.Milestone{}) +} + +func TestChangeMilestoneStatus(t *testing.T) { + assert.NoError(t, unittest.PrepareTestDatabase()) + milestone := unittest.AssertExistsAndLoadBean(t, &issues_model.Milestone{ID: 1}).(*issues_model.Milestone) + + assert.NoError(t, issues_model.ChangeMilestoneStatus(milestone, true)) + unittest.AssertExistsAndLoadBean(t, &issues_model.Milestone{ID: 1}, "is_closed=1") + unittest.CheckConsistencyFor(t, &repo_model.Repository{ID: milestone.RepoID}, &issues_model.Milestone{}) + + assert.NoError(t, issues_model.ChangeMilestoneStatus(milestone, false)) + unittest.AssertExistsAndLoadBean(t, &issues_model.Milestone{ID: 1}, "is_closed=0") + unittest.CheckConsistencyFor(t, &repo_model.Repository{ID: milestone.RepoID}, &issues_model.Milestone{}) +} + +func TestDeleteMilestoneByRepoID(t *testing.T) { + assert.NoError(t, unittest.PrepareTestDatabase()) + assert.NoError(t, issues_model.DeleteMilestoneByRepoID(1, 1)) + unittest.AssertNotExistsBean(t, &issues_model.Milestone{ID: 1}) + unittest.CheckConsistencyFor(t, &repo_model.Repository{ID: 1}) + + assert.NoError(t, issues_model.DeleteMilestoneByRepoID(unittest.NonexistentID, unittest.NonexistentID)) +} + +func TestUpdateMilestone(t *testing.T) { + assert.NoError(t, unittest.PrepareTestDatabase()) + + milestone := unittest.AssertExistsAndLoadBean(t, &issues_model.Milestone{ID: 1}).(*issues_model.Milestone) + milestone.Name = " newMilestoneName " + milestone.Content = "newMilestoneContent" + assert.NoError(t, issues_model.UpdateMilestone(milestone, milestone.IsClosed)) + milestone = unittest.AssertExistsAndLoadBean(t, &issues_model.Milestone{ID: 1}).(*issues_model.Milestone) + assert.EqualValues(t, "newMilestoneName", milestone.Name) + unittest.CheckConsistencyFor(t, &issues_model.Milestone{}) +} + +func TestUpdateMilestoneCounters(t *testing.T) { + assert.NoError(t, unittest.PrepareTestDatabase()) + issue := unittest.AssertExistsAndLoadBean(t, &issues_model.Issue{MilestoneID: 1}, + "is_closed=0").(*issues_model.Issue) + + issue.IsClosed = true + issue.ClosedUnix = timeutil.TimeStampNow() + _, err := db.GetEngine(db.DefaultContext).ID(issue.ID).Cols("is_closed", "closed_unix").Update(issue) + assert.NoError(t, err) + assert.NoError(t, issues_model.UpdateMilestoneCounters(db.DefaultContext, issue.MilestoneID)) + unittest.CheckConsistencyFor(t, &issues_model.Milestone{}) + + issue.IsClosed = false + issue.ClosedUnix = 0 + _, err = db.GetEngine(db.DefaultContext).ID(issue.ID).Cols("is_closed", "closed_unix").Update(issue) + assert.NoError(t, err) + assert.NoError(t, issues_model.UpdateMilestoneCounters(db.DefaultContext, issue.MilestoneID)) + unittest.CheckConsistencyFor(t, &issues_model.Milestone{}) +} diff --git a/models/pull.go b/models/issues/pull.go index 238eb16636..f2ca19b03e 100644 --- a/models/pull.go +++ b/models/issues/pull.go @@ -3,7 +3,7 @@ // Use of this source code is governed by a MIT-style // license that can be found in the LICENSE file. -package models +package issues import ( "context" @@ -25,6 +25,83 @@ import ( "xorm.io/builder" ) +// ErrPullRequestNotExist represents a "PullRequestNotExist" kind of error. +type ErrPullRequestNotExist struct { + ID int64 + IssueID int64 + HeadRepoID int64 + BaseRepoID int64 + HeadBranch string + BaseBranch string +} + +// IsErrPullRequestNotExist checks if an error is a ErrPullRequestNotExist. +func IsErrPullRequestNotExist(err error) bool { + _, ok := err.(ErrPullRequestNotExist) + return ok +} + +func (err ErrPullRequestNotExist) Error() string { + return fmt.Sprintf("pull request does not exist [id: %d, issue_id: %d, head_repo_id: %d, base_repo_id: %d, head_branch: %s, base_branch: %s]", + err.ID, err.IssueID, err.HeadRepoID, err.BaseRepoID, err.HeadBranch, err.BaseBranch) +} + +// ErrPullRequestAlreadyExists represents a "PullRequestAlreadyExists"-error +type ErrPullRequestAlreadyExists struct { + ID int64 + IssueID int64 + HeadRepoID int64 + BaseRepoID int64 + HeadBranch string + BaseBranch string +} + +// IsErrPullRequestAlreadyExists checks if an error is a ErrPullRequestAlreadyExists. +func IsErrPullRequestAlreadyExists(err error) bool { + _, ok := err.(ErrPullRequestAlreadyExists) + return ok +} + +// Error does pretty-printing :D +func (err ErrPullRequestAlreadyExists) Error() string { + return fmt.Sprintf("pull request already exists for these targets [id: %d, issue_id: %d, head_repo_id: %d, base_repo_id: %d, head_branch: %s, base_branch: %s]", + err.ID, err.IssueID, err.HeadRepoID, err.BaseRepoID, err.HeadBranch, err.BaseBranch) +} + +// ErrPullRequestHeadRepoMissing represents a "ErrPullRequestHeadRepoMissing" error +type ErrPullRequestHeadRepoMissing struct { + ID int64 + HeadRepoID int64 +} + +// IsErrErrPullRequestHeadRepoMissing checks if an error is a ErrPullRequestHeadRepoMissing. +func IsErrErrPullRequestHeadRepoMissing(err error) bool { + _, ok := err.(ErrPullRequestHeadRepoMissing) + return ok +} + +// Error does pretty-printing :D +func (err ErrPullRequestHeadRepoMissing) Error() string { + return fmt.Sprintf("pull request head repo missing [id: %d, head_repo_id: %d]", + err.ID, err.HeadRepoID) +} + +// ErrPullWasClosed is used close a closed pull request +type ErrPullWasClosed struct { + ID int64 + Index int64 +} + +// IsErrPullWasClosed checks if an error is a ErrErrPullWasClosed. +func IsErrPullWasClosed(err error) bool { + _, ok := err.(ErrPullWasClosed) + return ok +} + +func (err ErrPullWasClosed) Error() string { + return fmt.Sprintf("Pull request [%d] %d was already closed", err.ID, err.Index) +} + // PullRequestType defines pull request type type PullRequestType int @@ -98,7 +175,8 @@ func init() { db.RegisterModel(new(PullRequest)) } -func deletePullsByBaseRepoID(ctx context.Context, repoID int64) error { +// DeletePullsByBaseRepoID deletes all pull requests by the base repository ID +func DeletePullsByBaseRepoID(ctx context.Context, repoID int64) error { deleteCond := builder.Select("id").From("pull_request").Where(builder.Eq{"pull_request.base_repo_id": repoID}) // Delete scheduled auto merges @@ -219,7 +297,7 @@ func (pr *PullRequest) LoadIssueCtx(ctx context.Context) (err error) { return nil } - pr.Issue, err = getIssueByID(ctx, pr.IssueID) + pr.Issue, err = GetIssueByID(ctx, pr.IssueID) if err == nil { pr.Issue.PullRequest = pr } @@ -420,14 +498,14 @@ func NewPullRequest(outerCtx context.Context, repo *repo_model.Repository, issue defer committer.Close() ctx.WithContext(outerCtx) - if err = newIssue(ctx, issue.Poster, NewIssueOptions{ + if err = NewIssueWithIndex(ctx, issue.Poster, NewIssueOptions{ Repo: repo, Issue: issue, LabelIDs: labelIDs, Attachments: uuids, IsPull: true, }); err != nil { - if IsErrUserDoesNotHaveAccessToRepo(err) || IsErrNewIssueInsert(err) { + if repo_model.IsErrUserDoesNotHaveAccessToRepo(err) || IsErrNewIssueInsert(err) { return err } return fmt.Errorf("newIssue: %v", err) @@ -691,3 +769,70 @@ func (pr *PullRequest) Mergeable() bool { return pr.Status != PullRequestStatusChecking && pr.Status != PullRequestStatusConflict && pr.Status != PullRequestStatusError && !pr.IsWorkInProgress() } + +// HasEnoughApprovals returns true if pr has enough granted approvals. +func HasEnoughApprovals(ctx context.Context, protectBranch *git_model.ProtectedBranch, pr *PullRequest) bool { + if protectBranch.RequiredApprovals == 0 { + return true + } + return GetGrantedApprovalsCount(ctx, protectBranch, pr) >= protectBranch.RequiredApprovals +} + +// GetGrantedApprovalsCount returns the number of granted approvals for pr. A granted approval must be authored by a user in an approval whitelist. +func GetGrantedApprovalsCount(ctx context.Context, protectBranch *git_model.ProtectedBranch, pr *PullRequest) int64 { + sess := db.GetEngine(ctx).Where("issue_id = ?", pr.IssueID). + And("type = ?", ReviewTypeApprove). + And("official = ?", true). + And("dismissed = ?", false) + if protectBranch.DismissStaleApprovals { + sess = sess.And("stale = ?", false) + } + approvals, err := sess.Count(new(Review)) + if err != nil { + log.Error("GetGrantedApprovalsCount: %v", err) + return 0 + } + + return approvals +} + +// MergeBlockedByRejectedReview returns true if merge is blocked by rejected reviews +func MergeBlockedByRejectedReview(ctx context.Context, protectBranch *git_model.ProtectedBranch, pr *PullRequest) bool { + if !protectBranch.BlockOnRejectedReviews { + return false + } + rejectExist, err := db.GetEngine(ctx).Where("issue_id = ?", pr.IssueID). + And("type = ?", ReviewTypeReject). + And("official = ?", true). + And("dismissed = ?", false). + Exist(new(Review)) + if err != nil { + log.Error("MergeBlockedByRejectedReview: %v", err) + return true + } + + return rejectExist +} + +// MergeBlockedByOfficialReviewRequests block merge because of some review request to official reviewer +// of from official review +func MergeBlockedByOfficialReviewRequests(ctx context.Context, protectBranch *git_model.ProtectedBranch, pr *PullRequest) bool { + if !protectBranch.BlockOnOfficialReviewRequests { + return false + } + has, err := db.GetEngine(ctx).Where("issue_id = ?", pr.IssueID). + And("type = ?", ReviewTypeRequest). + And("official = ?", true). + Exist(new(Review)) + if err != nil { + log.Error("MergeBlockedByOfficialReviewRequests: %v", err) + return true + } + + return has +} + +// MergeBlockedByOutdatedBranch returns true if merge is blocked by an outdated head branch +func MergeBlockedByOutdatedBranch(protectBranch *git_model.ProtectedBranch, pr *PullRequest) bool { + return protectBranch.BlockOnOutdatedBranch && pr.CommitsBehind > 0 +} diff --git a/models/pull_list.go b/models/issues/pull_list.go index fb14d3beac..9ca536909e 100644 --- a/models/pull_list.go +++ b/models/issues/pull_list.go @@ -2,7 +2,7 @@ // Use of this source code is governed by a MIT-style // license that can be found in the LICENSE file. -package models +package issues import ( "context" diff --git a/models/pull_test.go b/models/issues/pull_test.go index 00bbfc798a..0d1991383d 100644 --- a/models/pull_test.go +++ b/models/issues/pull_test.go @@ -2,12 +2,13 @@ // Use of this source code is governed by a MIT-style // license that can be found in the LICENSE file. -package models +package issues_test import ( "testing" "code.gitea.io/gitea/models/db" + issues_model "code.gitea.io/gitea/models/issues" "code.gitea.io/gitea/models/unittest" "github.com/stretchr/testify/assert" @@ -15,7 +16,7 @@ import ( func TestPullRequest_LoadAttributes(t *testing.T) { assert.NoError(t, unittest.PrepareTestDatabase()) - pr := unittest.AssertExistsAndLoadBean(t, &PullRequest{ID: 1}).(*PullRequest) + pr := unittest.AssertExistsAndLoadBean(t, &issues_model.PullRequest{ID: 1}).(*issues_model.PullRequest) assert.NoError(t, pr.LoadAttributes()) assert.NotNil(t, pr.Merger) assert.Equal(t, pr.MergerID, pr.Merger.ID) @@ -23,7 +24,7 @@ func TestPullRequest_LoadAttributes(t *testing.T) { func TestPullRequest_LoadIssue(t *testing.T) { assert.NoError(t, unittest.PrepareTestDatabase()) - pr := unittest.AssertExistsAndLoadBean(t, &PullRequest{ID: 1}).(*PullRequest) + pr := unittest.AssertExistsAndLoadBean(t, &issues_model.PullRequest{ID: 1}).(*issues_model.PullRequest) assert.NoError(t, pr.LoadIssue()) assert.NotNil(t, pr.Issue) assert.Equal(t, int64(2), pr.Issue.ID) @@ -34,7 +35,7 @@ func TestPullRequest_LoadIssue(t *testing.T) { func TestPullRequest_LoadBaseRepo(t *testing.T) { assert.NoError(t, unittest.PrepareTestDatabase()) - pr := unittest.AssertExistsAndLoadBean(t, &PullRequest{ID: 1}).(*PullRequest) + pr := unittest.AssertExistsAndLoadBean(t, &issues_model.PullRequest{ID: 1}).(*issues_model.PullRequest) assert.NoError(t, pr.LoadBaseRepo()) assert.NotNil(t, pr.BaseRepo) assert.Equal(t, pr.BaseRepoID, pr.BaseRepo.ID) @@ -45,7 +46,7 @@ func TestPullRequest_LoadBaseRepo(t *testing.T) { func TestPullRequest_LoadHeadRepo(t *testing.T) { assert.NoError(t, unittest.PrepareTestDatabase()) - pr := unittest.AssertExistsAndLoadBean(t, &PullRequest{ID: 1}).(*PullRequest) + pr := unittest.AssertExistsAndLoadBean(t, &issues_model.PullRequest{ID: 1}).(*issues_model.PullRequest) assert.NoError(t, pr.LoadHeadRepo()) assert.NotNil(t, pr.HeadRepo) assert.Equal(t, pr.HeadRepoID, pr.HeadRepo.ID) @@ -57,7 +58,7 @@ func TestPullRequest_LoadHeadRepo(t *testing.T) { func TestPullRequestsNewest(t *testing.T) { assert.NoError(t, unittest.PrepareTestDatabase()) - prs, count, err := PullRequests(1, &PullRequestsOptions{ + prs, count, err := issues_model.PullRequests(1, &issues_model.PullRequestsOptions{ ListOptions: db.ListOptions{ Page: 1, }, @@ -76,7 +77,7 @@ func TestPullRequestsNewest(t *testing.T) { func TestPullRequestsOldest(t *testing.T) { assert.NoError(t, unittest.PrepareTestDatabase()) - prs, count, err := PullRequests(1, &PullRequestsOptions{ + prs, count, err := issues_model.PullRequests(1, &issues_model.PullRequestsOptions{ ListOptions: db.ListOptions{ Page: 1, }, @@ -95,30 +96,30 @@ func TestPullRequestsOldest(t *testing.T) { func TestGetUnmergedPullRequest(t *testing.T) { assert.NoError(t, unittest.PrepareTestDatabase()) - pr, err := GetUnmergedPullRequest(1, 1, "branch2", "master", PullRequestFlowGithub) + pr, err := issues_model.GetUnmergedPullRequest(1, 1, "branch2", "master", issues_model.PullRequestFlowGithub) assert.NoError(t, err) assert.Equal(t, int64(2), pr.ID) - _, err = GetUnmergedPullRequest(1, 9223372036854775807, "branch1", "master", PullRequestFlowGithub) + _, err = issues_model.GetUnmergedPullRequest(1, 9223372036854775807, "branch1", "master", issues_model.PullRequestFlowGithub) assert.Error(t, err) - assert.True(t, IsErrPullRequestNotExist(err)) + assert.True(t, issues_model.IsErrPullRequestNotExist(err)) } func TestHasUnmergedPullRequestsByHeadInfo(t *testing.T) { assert.NoError(t, unittest.PrepareTestDatabase()) - exist, err := HasUnmergedPullRequestsByHeadInfo(db.DefaultContext, 1, "branch2") + exist, err := issues_model.HasUnmergedPullRequestsByHeadInfo(db.DefaultContext, 1, "branch2") assert.NoError(t, err) assert.Equal(t, true, exist) - exist, err = HasUnmergedPullRequestsByHeadInfo(db.DefaultContext, 1, "not_exist_branch") + exist, err = issues_model.HasUnmergedPullRequestsByHeadInfo(db.DefaultContext, 1, "not_exist_branch") assert.NoError(t, err) assert.Equal(t, false, exist) } func TestGetUnmergedPullRequestsByHeadInfo(t *testing.T) { assert.NoError(t, unittest.PrepareTestDatabase()) - prs, err := GetUnmergedPullRequestsByHeadInfo(1, "branch2") + prs, err := issues_model.GetUnmergedPullRequestsByHeadInfo(1, "branch2") assert.NoError(t, err) assert.Len(t, prs, 1) for _, pr := range prs { @@ -129,7 +130,7 @@ func TestGetUnmergedPullRequestsByHeadInfo(t *testing.T) { func TestGetUnmergedPullRequestsByBaseInfo(t *testing.T) { assert.NoError(t, unittest.PrepareTestDatabase()) - prs, err := GetUnmergedPullRequestsByBaseInfo(1, "master") + prs, err := issues_model.GetUnmergedPullRequestsByBaseInfo(1, "master") assert.NoError(t, err) assert.Len(t, prs, 1) pr := prs[0] @@ -140,51 +141,51 @@ func TestGetUnmergedPullRequestsByBaseInfo(t *testing.T) { func TestGetPullRequestByIndex(t *testing.T) { assert.NoError(t, unittest.PrepareTestDatabase()) - pr, err := GetPullRequestByIndex(db.DefaultContext, 1, 2) + pr, err := issues_model.GetPullRequestByIndex(db.DefaultContext, 1, 2) assert.NoError(t, err) assert.Equal(t, int64(1), pr.BaseRepoID) assert.Equal(t, int64(2), pr.Index) - _, err = GetPullRequestByIndex(db.DefaultContext, 9223372036854775807, 9223372036854775807) + _, err = issues_model.GetPullRequestByIndex(db.DefaultContext, 9223372036854775807, 9223372036854775807) assert.Error(t, err) - assert.True(t, IsErrPullRequestNotExist(err)) + assert.True(t, issues_model.IsErrPullRequestNotExist(err)) - _, err = GetPullRequestByIndex(db.DefaultContext, 1, 0) + _, err = issues_model.GetPullRequestByIndex(db.DefaultContext, 1, 0) assert.Error(t, err) - assert.True(t, IsErrPullRequestNotExist(err)) + assert.True(t, issues_model.IsErrPullRequestNotExist(err)) } func TestGetPullRequestByID(t *testing.T) { assert.NoError(t, unittest.PrepareTestDatabase()) - pr, err := GetPullRequestByID(db.DefaultContext, 1) + pr, err := issues_model.GetPullRequestByID(db.DefaultContext, 1) assert.NoError(t, err) assert.Equal(t, int64(1), pr.ID) assert.Equal(t, int64(2), pr.IssueID) - _, err = GetPullRequestByID(db.DefaultContext, 9223372036854775807) + _, err = issues_model.GetPullRequestByID(db.DefaultContext, 9223372036854775807) assert.Error(t, err) - assert.True(t, IsErrPullRequestNotExist(err)) + assert.True(t, issues_model.IsErrPullRequestNotExist(err)) } func TestGetPullRequestByIssueID(t *testing.T) { assert.NoError(t, unittest.PrepareTestDatabase()) - pr, err := GetPullRequestByIssueID(db.DefaultContext, 2) + pr, err := issues_model.GetPullRequestByIssueID(db.DefaultContext, 2) assert.NoError(t, err) assert.Equal(t, int64(2), pr.IssueID) - _, err = GetPullRequestByIssueID(db.DefaultContext, 9223372036854775807) + _, err = issues_model.GetPullRequestByIssueID(db.DefaultContext, 9223372036854775807) assert.Error(t, err) - assert.True(t, IsErrPullRequestNotExist(err)) + assert.True(t, issues_model.IsErrPullRequestNotExist(err)) } func TestPullRequest_Update(t *testing.T) { assert.NoError(t, unittest.PrepareTestDatabase()) - pr := unittest.AssertExistsAndLoadBean(t, &PullRequest{ID: 1}).(*PullRequest) + pr := unittest.AssertExistsAndLoadBean(t, &issues_model.PullRequest{ID: 1}).(*issues_model.PullRequest) pr.BaseBranch = "baseBranch" pr.HeadBranch = "headBranch" pr.Update() - pr = unittest.AssertExistsAndLoadBean(t, &PullRequest{ID: pr.ID}).(*PullRequest) + pr = unittest.AssertExistsAndLoadBean(t, &issues_model.PullRequest{ID: pr.ID}).(*issues_model.PullRequest) assert.Equal(t, "baseBranch", pr.BaseBranch) assert.Equal(t, "headBranch", pr.HeadBranch) unittest.CheckConsistencyFor(t, pr) @@ -192,14 +193,14 @@ func TestPullRequest_Update(t *testing.T) { func TestPullRequest_UpdateCols(t *testing.T) { assert.NoError(t, unittest.PrepareTestDatabase()) - pr := &PullRequest{ + pr := &issues_model.PullRequest{ ID: 1, BaseBranch: "baseBranch", HeadBranch: "headBranch", } assert.NoError(t, pr.UpdateCols("head_branch")) - pr = unittest.AssertExistsAndLoadBean(t, &PullRequest{ID: 1}).(*PullRequest) + pr = unittest.AssertExistsAndLoadBean(t, &issues_model.PullRequest{ID: 1}).(*issues_model.PullRequest) assert.Equal(t, "master", pr.BaseBranch) assert.Equal(t, "headBranch", pr.HeadBranch) unittest.CheckConsistencyFor(t, pr) @@ -208,17 +209,17 @@ func TestPullRequest_UpdateCols(t *testing.T) { func TestPullRequestList_LoadAttributes(t *testing.T) { assert.NoError(t, unittest.PrepareTestDatabase()) - prs := []*PullRequest{ - unittest.AssertExistsAndLoadBean(t, &PullRequest{ID: 1}).(*PullRequest), - unittest.AssertExistsAndLoadBean(t, &PullRequest{ID: 2}).(*PullRequest), + prs := []*issues_model.PullRequest{ + unittest.AssertExistsAndLoadBean(t, &issues_model.PullRequest{ID: 1}).(*issues_model.PullRequest), + unittest.AssertExistsAndLoadBean(t, &issues_model.PullRequest{ID: 2}).(*issues_model.PullRequest), } - assert.NoError(t, PullRequestList(prs).LoadAttributes()) + assert.NoError(t, issues_model.PullRequestList(prs).LoadAttributes()) for _, pr := range prs { assert.NotNil(t, pr.Issue) assert.Equal(t, pr.IssueID, pr.Issue.ID) } - assert.NoError(t, PullRequestList([]*PullRequest{}).LoadAttributes()) + assert.NoError(t, issues_model.PullRequestList([]*issues_model.PullRequest{}).LoadAttributes()) } // TODO TestAddTestPullRequestTask @@ -226,7 +227,7 @@ func TestPullRequestList_LoadAttributes(t *testing.T) { func TestPullRequest_IsWorkInProgress(t *testing.T) { assert.NoError(t, unittest.PrepareTestDatabase()) - pr := unittest.AssertExistsAndLoadBean(t, &PullRequest{ID: 2}).(*PullRequest) + pr := unittest.AssertExistsAndLoadBean(t, &issues_model.PullRequest{ID: 2}).(*issues_model.PullRequest) pr.LoadIssue() assert.False(t, pr.IsWorkInProgress()) @@ -241,7 +242,7 @@ func TestPullRequest_IsWorkInProgress(t *testing.T) { func TestPullRequest_GetWorkInProgressPrefixWorkInProgress(t *testing.T) { assert.NoError(t, unittest.PrepareTestDatabase()) - pr := unittest.AssertExistsAndLoadBean(t, &PullRequest{ID: 2}).(*PullRequest) + pr := unittest.AssertExistsAndLoadBean(t, &issues_model.PullRequest{ID: 2}).(*issues_model.PullRequest) pr.LoadIssue() assert.Empty(t, pr.GetWorkInProgressPrefix()) @@ -253,3 +254,24 @@ func TestPullRequest_GetWorkInProgressPrefixWorkInProgress(t *testing.T) { pr.Issue.Title = "[wip] " + original assert.Equal(t, "[wip]", pr.GetWorkInProgressPrefix()) } + +func TestDeleteOrphanedObjects(t *testing.T) { + assert.NoError(t, unittest.PrepareTestDatabase()) + + countBefore, err := db.GetEngine(db.DefaultContext).Count(&issues_model.PullRequest{}) + assert.NoError(t, err) + + _, err = db.GetEngine(db.DefaultContext).Insert(&issues_model.PullRequest{IssueID: 1000}, &issues_model.PullRequest{IssueID: 1001}, &issues_model.PullRequest{IssueID: 1003}) + assert.NoError(t, err) + + orphaned, err := db.CountOrphanedObjects("pull_request", "issue", "pull_request.issue_id=issue.id") + assert.NoError(t, err) + assert.EqualValues(t, 3, orphaned) + + err = db.DeleteOrphanedObjects("pull_request", "issue", "pull_request.issue_id=issue.id") + assert.NoError(t, err) + + countAfter, err := db.GetEngine(db.DefaultContext).Count(&issues_model.PullRequest{}) + assert.NoError(t, err) + assert.EqualValues(t, countBefore, countAfter) +} diff --git a/models/issues/reaction_test.go b/models/issues/reaction_test.go index b1216a3a69..ee1b6687a2 100644 --- a/models/issues/reaction_test.go +++ b/models/issues/reaction_test.go @@ -2,12 +2,13 @@ // Use of this source code is governed by a MIT-style // license that can be found in the LICENSE file. -package issues +package issues_test import ( "testing" "code.gitea.io/gitea/models/db" + issues_model "code.gitea.io/gitea/models/issues" repo_model "code.gitea.io/gitea/models/repo" "code.gitea.io/gitea/models/unittest" user_model "code.gitea.io/gitea/models/user" @@ -17,12 +18,12 @@ import ( ) func addReaction(t *testing.T, doerID, issueID, commentID int64, content string) { - var reaction *Reaction + var reaction *issues_model.Reaction var err error if commentID == 0 { - reaction, err = CreateIssueReaction(doerID, issueID, content) + reaction, err = issues_model.CreateIssueReaction(doerID, issueID, content) } else { - reaction, err = CreateCommentReaction(doerID, issueID, commentID, content) + reaction, err = issues_model.CreateCommentReaction(doerID, issueID, commentID, content) } assert.NoError(t, err) assert.NotNil(t, reaction) @@ -37,7 +38,7 @@ func TestIssueAddReaction(t *testing.T) { addReaction(t, user1.ID, issue1ID, 0, "heart") - unittest.AssertExistsAndLoadBean(t, &Reaction{Type: "heart", UserID: user1.ID, IssueID: issue1ID}) + unittest.AssertExistsAndLoadBean(t, &issues_model.Reaction{Type: "heart", UserID: user1.ID, IssueID: issue1ID}) } func TestIssueAddDuplicateReaction(t *testing.T) { @@ -49,15 +50,15 @@ func TestIssueAddDuplicateReaction(t *testing.T) { addReaction(t, user1.ID, issue1ID, 0, "heart") - reaction, err := CreateReaction(&ReactionOptions{ + reaction, err := issues_model.CreateReaction(&issues_model.ReactionOptions{ DoerID: user1.ID, IssueID: issue1ID, Type: "heart", }) assert.Error(t, err) - assert.Equal(t, ErrReactionAlreadyExist{Reaction: "heart"}, err) + assert.Equal(t, issues_model.ErrReactionAlreadyExist{Reaction: "heart"}, err) - existingR := unittest.AssertExistsAndLoadBean(t, &Reaction{Type: "heart", UserID: user1.ID, IssueID: issue1ID}).(*Reaction) + existingR := unittest.AssertExistsAndLoadBean(t, &issues_model.Reaction{Type: "heart", UserID: user1.ID, IssueID: issue1ID}).(*issues_model.Reaction) assert.Equal(t, existingR.ID, reaction.ID) } @@ -70,10 +71,10 @@ func TestIssueDeleteReaction(t *testing.T) { addReaction(t, user1.ID, issue1ID, 0, "heart") - err := DeleteIssueReaction(user1.ID, issue1ID, "heart") + err := issues_model.DeleteIssueReaction(user1.ID, issue1ID, "heart") assert.NoError(t, err) - unittest.AssertNotExistsBean(t, &Reaction{Type: "heart", UserID: user1.ID, IssueID: issue1ID}) + unittest.AssertNotExistsBean(t, &issues_model.Reaction{Type: "heart", UserID: user1.ID, IssueID: issue1ID}) } func TestIssueReactionCount(t *testing.T) { @@ -98,7 +99,7 @@ func TestIssueReactionCount(t *testing.T) { addReaction(t, user4.ID, issueID, 0, "heart") addReaction(t, ghost.ID, issueID, 0, "-1") - reactionsList, _, err := FindReactions(db.DefaultContext, FindReactionsOptions{ + reactionsList, _, err := issues_model.FindReactions(db.DefaultContext, issues_model.FindReactionsOptions{ IssueID: issueID, }) assert.NoError(t, err) @@ -128,7 +129,7 @@ func TestIssueCommentAddReaction(t *testing.T) { addReaction(t, user1.ID, issue1ID, comment1ID, "heart") - unittest.AssertExistsAndLoadBean(t, &Reaction{Type: "heart", UserID: user1.ID, IssueID: issue1ID, CommentID: comment1ID}) + unittest.AssertExistsAndLoadBean(t, &issues_model.Reaction{Type: "heart", UserID: user1.ID, IssueID: issue1ID, CommentID: comment1ID}) } func TestIssueCommentDeleteReaction(t *testing.T) { @@ -147,7 +148,7 @@ func TestIssueCommentDeleteReaction(t *testing.T) { addReaction(t, user3.ID, issue1ID, comment1ID, "heart") addReaction(t, user4.ID, issue1ID, comment1ID, "+1") - reactionsList, _, err := FindReactions(db.DefaultContext, FindReactionsOptions{ + reactionsList, _, err := issues_model.FindReactions(db.DefaultContext, issues_model.FindReactionsOptions{ IssueID: issue1ID, CommentID: comment1ID, }) @@ -168,7 +169,7 @@ func TestIssueCommentReactionCount(t *testing.T) { var comment1ID int64 = 1 addReaction(t, user1.ID, issue1ID, comment1ID, "heart") - assert.NoError(t, DeleteCommentReaction(user1.ID, issue1ID, comment1ID, "heart")) + assert.NoError(t, issues_model.DeleteCommentReaction(user1.ID, issue1ID, comment1ID, "heart")) - unittest.AssertNotExistsBean(t, &Reaction{Type: "heart", UserID: user1.ID, IssueID: issue1ID, CommentID: comment1ID}) + unittest.AssertNotExistsBean(t, &issues_model.Reaction{Type: "heart", UserID: user1.ID, IssueID: issue1ID, CommentID: comment1ID}) } diff --git a/models/review.go b/models/issues/review.go index e92caba938..ee65bec3f8 100644 --- a/models/review.go +++ b/models/issues/review.go @@ -2,7 +2,7 @@ // Use of this source code is governed by a MIT-style // license that can be found in the LICENSE file. -package models +package issues import ( "context" @@ -17,11 +17,47 @@ import ( "code.gitea.io/gitea/models/unit" user_model "code.gitea.io/gitea/models/user" "code.gitea.io/gitea/modules/base" + "code.gitea.io/gitea/modules/structs" "code.gitea.io/gitea/modules/timeutil" "xorm.io/builder" ) +// ErrReviewNotExist represents a "ReviewNotExist" kind of error. +type ErrReviewNotExist struct { + ID int64 +} + +// IsErrReviewNotExist checks if an error is a ErrReviewNotExist. +func IsErrReviewNotExist(err error) bool { + _, ok := err.(ErrReviewNotExist) + return ok +} + +func (err ErrReviewNotExist) Error() string { + return fmt.Sprintf("review does not exist [id: %d]", err.ID) +} + +// ErrNotValidReviewRequest an not allowed review request modify +type ErrNotValidReviewRequest struct { + Reason string + UserID int64 + RepoID int64 +} + +// IsErrNotValidReviewRequest checks if an error is a ErrNotValidReviewRequest. +func IsErrNotValidReviewRequest(err error) bool { + _, ok := err.(ErrNotValidReviewRequest) + return ok +} + +func (err ErrNotValidReviewRequest) Error() string { + return fmt.Sprintf("%s [user_id: %d, repo_id: %d]", + err.Reason, + err.UserID, + err.RepoID) +} + // ReviewType defines the sort of feedback a review gives type ReviewType int @@ -105,7 +141,7 @@ func (r *Review) loadIssue(ctx context.Context) (err error) { if r.Issue != nil { return } - r.Issue, err = getIssueByID(ctx, r.IssueID) + r.Issue, err = GetIssueByID(ctx, r.IssueID) return } @@ -967,3 +1003,16 @@ func (r *Review) GetExternalName() string { return r.OriginalAuthor } // GetExternalID ExternalUserRemappable interface func (r *Review) GetExternalID() int64 { return r.OriginalAuthorID } + +// UpdateReviewsMigrationsByType updates reviews' migrations information via given git service type and original id and poster id +func UpdateReviewsMigrationsByType(tp structs.GitServiceType, originalAuthorID string, posterID int64) error { + _, err := db.GetEngine(db.DefaultContext).Table("review"). + Where("original_author_id = ?", originalAuthorID). + And(migratedIssueCond(tp)). + Update(map[string]interface{}{ + "reviewer_id": posterID, + "original_author": "", + "original_author_id": 0, + }) + return err +} diff --git a/models/review_test.go b/models/issues/review_test.go index 93291f9f57..3506604b46 100644 --- a/models/review_test.go +++ b/models/issues/review_test.go @@ -2,12 +2,13 @@ // Use of this source code is governed by a MIT-style // license that can be found in the LICENSE file. -package models +package issues_test import ( "testing" "code.gitea.io/gitea/models/db" + issues_model "code.gitea.io/gitea/models/issues" "code.gitea.io/gitea/models/unittest" user_model "code.gitea.io/gitea/models/user" @@ -16,34 +17,34 @@ import ( func TestGetReviewByID(t *testing.T) { assert.NoError(t, unittest.PrepareTestDatabase()) - review, err := GetReviewByID(db.DefaultContext, 1) + review, err := issues_model.GetReviewByID(db.DefaultContext, 1) assert.NoError(t, err) assert.Equal(t, "Demo Review", review.Content) - assert.Equal(t, ReviewTypeApprove, review.Type) + assert.Equal(t, issues_model.ReviewTypeApprove, review.Type) - _, err = GetReviewByID(db.DefaultContext, 23892) + _, err = issues_model.GetReviewByID(db.DefaultContext, 23892) assert.Error(t, err) - assert.True(t, IsErrReviewNotExist(err), "IsErrReviewNotExist") + assert.True(t, issues_model.IsErrReviewNotExist(err), "IsErrReviewNotExist") } func TestReview_LoadAttributes(t *testing.T) { assert.NoError(t, unittest.PrepareTestDatabase()) - review := unittest.AssertExistsAndLoadBean(t, &Review{ID: 1}).(*Review) + review := unittest.AssertExistsAndLoadBean(t, &issues_model.Review{ID: 1}).(*issues_model.Review) assert.NoError(t, review.LoadAttributes(db.DefaultContext)) assert.NotNil(t, review.Issue) assert.NotNil(t, review.Reviewer) - invalidReview1 := unittest.AssertExistsAndLoadBean(t, &Review{ID: 2}).(*Review) + invalidReview1 := unittest.AssertExistsAndLoadBean(t, &issues_model.Review{ID: 2}).(*issues_model.Review) assert.Error(t, invalidReview1.LoadAttributes(db.DefaultContext)) - invalidReview2 := unittest.AssertExistsAndLoadBean(t, &Review{ID: 3}).(*Review) + invalidReview2 := unittest.AssertExistsAndLoadBean(t, &issues_model.Review{ID: 3}).(*issues_model.Review) assert.Error(t, invalidReview2.LoadAttributes(db.DefaultContext)) } func TestReview_LoadCodeComments(t *testing.T) { assert.NoError(t, unittest.PrepareTestDatabase()) - review := unittest.AssertExistsAndLoadBean(t, &Review{ID: 4}).(*Review) + review := unittest.AssertExistsAndLoadBean(t, &issues_model.Review{ID: 4}).(*issues_model.Review) assert.NoError(t, review.LoadAttributes(db.DefaultContext)) assert.NoError(t, review.LoadCodeComments(db.DefaultContext)) assert.Len(t, review.CodeComments, 1) @@ -51,18 +52,18 @@ func TestReview_LoadCodeComments(t *testing.T) { } func TestReviewType_Icon(t *testing.T) { - assert.Equal(t, "check", ReviewTypeApprove.Icon()) - assert.Equal(t, "diff", ReviewTypeReject.Icon()) - assert.Equal(t, "comment", ReviewTypeComment.Icon()) - assert.Equal(t, "comment", ReviewTypeUnknown.Icon()) - assert.Equal(t, "dot-fill", ReviewTypeRequest.Icon()) - assert.Equal(t, "comment", ReviewType(6).Icon()) + assert.Equal(t, "check", issues_model.ReviewTypeApprove.Icon()) + assert.Equal(t, "diff", issues_model.ReviewTypeReject.Icon()) + assert.Equal(t, "comment", issues_model.ReviewTypeComment.Icon()) + assert.Equal(t, "comment", issues_model.ReviewTypeUnknown.Icon()) + assert.Equal(t, "dot-fill", issues_model.ReviewTypeRequest.Icon()) + assert.Equal(t, "comment", issues_model.ReviewType(6).Icon()) } func TestFindReviews(t *testing.T) { assert.NoError(t, unittest.PrepareTestDatabase()) - reviews, err := FindReviews(db.DefaultContext, FindReviewOptions{ - Type: ReviewTypeApprove, + reviews, err := issues_model.FindReviews(db.DefaultContext, issues_model.FindReviewOptions{ + Type: issues_model.ReviewTypeApprove, IssueID: 2, ReviewerID: 1, }) @@ -73,66 +74,66 @@ func TestFindReviews(t *testing.T) { func TestGetCurrentReview(t *testing.T) { assert.NoError(t, unittest.PrepareTestDatabase()) - issue := unittest.AssertExistsAndLoadBean(t, &Issue{ID: 2}).(*Issue) + issue := unittest.AssertExistsAndLoadBean(t, &issues_model.Issue{ID: 2}).(*issues_model.Issue) user := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 1}).(*user_model.User) - review, err := GetCurrentReview(db.DefaultContext, user, issue) + review, err := issues_model.GetCurrentReview(db.DefaultContext, user, issue) assert.NoError(t, err) assert.NotNil(t, review) - assert.Equal(t, ReviewTypePending, review.Type) + assert.Equal(t, issues_model.ReviewTypePending, review.Type) assert.Equal(t, "Pending Review", review.Content) user2 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 7}).(*user_model.User) - review2, err := GetCurrentReview(db.DefaultContext, user2, issue) + review2, err := issues_model.GetCurrentReview(db.DefaultContext, user2, issue) assert.Error(t, err) - assert.True(t, IsErrReviewNotExist(err)) + assert.True(t, issues_model.IsErrReviewNotExist(err)) assert.Nil(t, review2) } func TestCreateReview(t *testing.T) { assert.NoError(t, unittest.PrepareTestDatabase()) - issue := unittest.AssertExistsAndLoadBean(t, &Issue{ID: 2}).(*Issue) + issue := unittest.AssertExistsAndLoadBean(t, &issues_model.Issue{ID: 2}).(*issues_model.Issue) user := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 1}).(*user_model.User) - review, err := CreateReview(db.DefaultContext, CreateReviewOptions{ + review, err := issues_model.CreateReview(db.DefaultContext, issues_model.CreateReviewOptions{ Content: "New Review", - Type: ReviewTypePending, + Type: issues_model.ReviewTypePending, Issue: issue, Reviewer: user, }) assert.NoError(t, err) assert.Equal(t, "New Review", review.Content) - unittest.AssertExistsAndLoadBean(t, &Review{Content: "New Review"}) + unittest.AssertExistsAndLoadBean(t, &issues_model.Review{Content: "New Review"}) } func TestGetReviewersByIssueID(t *testing.T) { assert.NoError(t, unittest.PrepareTestDatabase()) - issue := unittest.AssertExistsAndLoadBean(t, &Issue{ID: 3}).(*Issue) + issue := unittest.AssertExistsAndLoadBean(t, &issues_model.Issue{ID: 3}).(*issues_model.Issue) user2 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2}).(*user_model.User) user3 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 3}).(*user_model.User) user4 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 4}).(*user_model.User) - expectedReviews := []*Review{} + expectedReviews := []*issues_model.Review{} expectedReviews = append(expectedReviews, - &Review{ + &issues_model.Review{ Reviewer: user3, - Type: ReviewTypeReject, + Type: issues_model.ReviewTypeReject, UpdatedUnix: 946684812, }, - &Review{ + &issues_model.Review{ Reviewer: user4, - Type: ReviewTypeApprove, + Type: issues_model.ReviewTypeApprove, UpdatedUnix: 946684813, }, - &Review{ + &issues_model.Review{ Reviewer: user2, - Type: ReviewTypeReject, + Type: issues_model.ReviewTypeReject, UpdatedUnix: 946684814, }) - allReviews, err := GetReviewersByIssueID(issue.ID) + allReviews, err := issues_model.GetReviewersByIssueID(issue.ID) for _, reviewer := range allReviews { assert.NoError(t, reviewer.LoadReviewer()) } @@ -149,53 +150,53 @@ func TestGetReviewersByIssueID(t *testing.T) { func TestDismissReview(t *testing.T) { assert.NoError(t, unittest.PrepareTestDatabase()) - rejectReviewExample := unittest.AssertExistsAndLoadBean(t, &Review{ID: 9}).(*Review) - requestReviewExample := unittest.AssertExistsAndLoadBean(t, &Review{ID: 11}).(*Review) - approveReviewExample := unittest.AssertExistsAndLoadBean(t, &Review{ID: 8}).(*Review) + rejectReviewExample := unittest.AssertExistsAndLoadBean(t, &issues_model.Review{ID: 9}).(*issues_model.Review) + requestReviewExample := unittest.AssertExistsAndLoadBean(t, &issues_model.Review{ID: 11}).(*issues_model.Review) + approveReviewExample := unittest.AssertExistsAndLoadBean(t, &issues_model.Review{ID: 8}).(*issues_model.Review) assert.False(t, rejectReviewExample.Dismissed) assert.False(t, requestReviewExample.Dismissed) assert.False(t, approveReviewExample.Dismissed) - assert.NoError(t, DismissReview(rejectReviewExample, true)) - rejectReviewExample = unittest.AssertExistsAndLoadBean(t, &Review{ID: 9}).(*Review) - requestReviewExample = unittest.AssertExistsAndLoadBean(t, &Review{ID: 11}).(*Review) + assert.NoError(t, issues_model.DismissReview(rejectReviewExample, true)) + rejectReviewExample = unittest.AssertExistsAndLoadBean(t, &issues_model.Review{ID: 9}).(*issues_model.Review) + requestReviewExample = unittest.AssertExistsAndLoadBean(t, &issues_model.Review{ID: 11}).(*issues_model.Review) assert.True(t, rejectReviewExample.Dismissed) assert.False(t, requestReviewExample.Dismissed) - assert.NoError(t, DismissReview(requestReviewExample, true)) - rejectReviewExample = unittest.AssertExistsAndLoadBean(t, &Review{ID: 9}).(*Review) - requestReviewExample = unittest.AssertExistsAndLoadBean(t, &Review{ID: 11}).(*Review) + assert.NoError(t, issues_model.DismissReview(requestReviewExample, true)) + rejectReviewExample = unittest.AssertExistsAndLoadBean(t, &issues_model.Review{ID: 9}).(*issues_model.Review) + requestReviewExample = unittest.AssertExistsAndLoadBean(t, &issues_model.Review{ID: 11}).(*issues_model.Review) assert.True(t, rejectReviewExample.Dismissed) assert.False(t, requestReviewExample.Dismissed) assert.False(t, approveReviewExample.Dismissed) - assert.NoError(t, DismissReview(requestReviewExample, true)) - rejectReviewExample = unittest.AssertExistsAndLoadBean(t, &Review{ID: 9}).(*Review) - requestReviewExample = unittest.AssertExistsAndLoadBean(t, &Review{ID: 11}).(*Review) + assert.NoError(t, issues_model.DismissReview(requestReviewExample, true)) + rejectReviewExample = unittest.AssertExistsAndLoadBean(t, &issues_model.Review{ID: 9}).(*issues_model.Review) + requestReviewExample = unittest.AssertExistsAndLoadBean(t, &issues_model.Review{ID: 11}).(*issues_model.Review) assert.True(t, rejectReviewExample.Dismissed) assert.False(t, requestReviewExample.Dismissed) assert.False(t, approveReviewExample.Dismissed) - assert.NoError(t, DismissReview(requestReviewExample, false)) - rejectReviewExample = unittest.AssertExistsAndLoadBean(t, &Review{ID: 9}).(*Review) - requestReviewExample = unittest.AssertExistsAndLoadBean(t, &Review{ID: 11}).(*Review) + assert.NoError(t, issues_model.DismissReview(requestReviewExample, false)) + rejectReviewExample = unittest.AssertExistsAndLoadBean(t, &issues_model.Review{ID: 9}).(*issues_model.Review) + requestReviewExample = unittest.AssertExistsAndLoadBean(t, &issues_model.Review{ID: 11}).(*issues_model.Review) assert.True(t, rejectReviewExample.Dismissed) assert.False(t, requestReviewExample.Dismissed) assert.False(t, approveReviewExample.Dismissed) - assert.NoError(t, DismissReview(requestReviewExample, false)) - rejectReviewExample = unittest.AssertExistsAndLoadBean(t, &Review{ID: 9}).(*Review) - requestReviewExample = unittest.AssertExistsAndLoadBean(t, &Review{ID: 11}).(*Review) + assert.NoError(t, issues_model.DismissReview(requestReviewExample, false)) + rejectReviewExample = unittest.AssertExistsAndLoadBean(t, &issues_model.Review{ID: 9}).(*issues_model.Review) + requestReviewExample = unittest.AssertExistsAndLoadBean(t, &issues_model.Review{ID: 11}).(*issues_model.Review) assert.True(t, rejectReviewExample.Dismissed) assert.False(t, requestReviewExample.Dismissed) assert.False(t, approveReviewExample.Dismissed) - assert.NoError(t, DismissReview(rejectReviewExample, false)) + assert.NoError(t, issues_model.DismissReview(rejectReviewExample, false)) assert.False(t, rejectReviewExample.Dismissed) assert.False(t, requestReviewExample.Dismissed) assert.False(t, approveReviewExample.Dismissed) - assert.NoError(t, DismissReview(approveReviewExample, true)) + assert.NoError(t, issues_model.DismissReview(approveReviewExample, true)) assert.False(t, rejectReviewExample.Dismissed) assert.False(t, requestReviewExample.Dismissed) assert.True(t, approveReviewExample.Dismissed) diff --git a/models/issue_stopwatch.go b/models/issues/stopwatch.go index 2cb4a62bd3..e7ac1314e9 100644 --- a/models/issue_stopwatch.go +++ b/models/issues/stopwatch.go @@ -2,7 +2,7 @@ // Use of this source code is governed by a MIT-style // license that can be found in the LICENSE file. -package models +package issues import ( "context" @@ -215,7 +215,7 @@ func CreateIssueStopwatch(ctx context.Context, user *user_model.User, issue *Iss return err } if exists { - issue, err := getIssueByID(ctx, sw.IssueID) + issue, err := GetIssueByID(ctx, sw.IssueID) if err != nil { return err } diff --git a/models/issues/stopwatch_test.go b/models/issues/stopwatch_test.go new file mode 100644 index 0000000000..c0573964d5 --- /dev/null +++ b/models/issues/stopwatch_test.go @@ -0,0 +1,79 @@ +// Copyright 2020 The Gitea Authors. All rights reserved. +// Use of this source code is governed by a MIT-style +// license that can be found in the LICENSE file. + +package issues_test + +import ( + "testing" + + "code.gitea.io/gitea/models/db" + 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" +) + +func TestCancelStopwatch(t *testing.T) { + assert.NoError(t, unittest.PrepareTestDatabase()) + + user1, err := user_model.GetUserByID(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) + + err = issues_model.CancelStopwatch(user1, issue1) + assert.NoError(t, err) + 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}) + + assert.Nil(t, issues_model.CancelStopwatch(user1, issue2)) +} + +func TestStopwatchExists(t *testing.T) { + assert.NoError(t, unittest.PrepareTestDatabase()) + + assert.True(t, issues_model.StopwatchExists(1, 1)) + assert.False(t, issues_model.StopwatchExists(1, 2)) +} + +func TestHasUserStopwatch(t *testing.T) { + assert.NoError(t, unittest.PrepareTestDatabase()) + + exists, sw, err := issues_model.HasUserStopwatch(db.DefaultContext, 1) + assert.NoError(t, err) + assert.True(t, exists) + assert.Equal(t, int64(1), sw.ID) + + exists, _, err = issues_model.HasUserStopwatch(db.DefaultContext, 3) + assert.NoError(t, err) + assert.False(t, exists) +} + +func TestCreateOrStopIssueStopwatch(t *testing.T) { + assert.NoError(t, unittest.PrepareTestDatabase()) + + user2, err := user_model.GetUserByID(2) + assert.NoError(t, err) + user3, err := user_model.GetUserByID(3) + 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) + + assert.NoError(t, issues_model.CreateOrStopIssueStopwatch(user3, issue1)) + sw := unittest.AssertExistsAndLoadBean(t, &issues_model.Stopwatch{UserID: 3, IssueID: 1}).(*issues_model.Stopwatch) + assert.LessOrEqual(t, sw.CreatedUnix, timeutil.TimeStampNow()) + + assert.NoError(t, issues_model.CreateOrStopIssueStopwatch(user2, issue2)) + unittest.AssertNotExistsBean(t, &issues_model.Stopwatch{UserID: 2, IssueID: 2}) + unittest.AssertExistsAndLoadBean(t, &issues_model.TrackedTime{UserID: 2, IssueID: 2}) +} diff --git a/models/issue_tracked_time.go b/models/issues/tracked_time.go index 30b3905bbc..54179bd3ab 100644 --- a/models/issue_tracked_time.go +++ b/models/issues/tracked_time.go @@ -2,7 +2,7 @@ // Use of this source code is governed by a MIT-style // license that can be found in the LICENSE file. -package models +package issues import ( "context" @@ -48,7 +48,7 @@ func (t *TrackedTime) LoadAttributes() (err error) { func (t *TrackedTime) loadAttributes(ctx context.Context) (err error) { if t.Issue == nil { - t.Issue, err = getIssueByID(ctx, t.IssueID) + t.Issue, err = GetIssueByID(ctx, t.IssueID) if err != nil { return } diff --git a/models/issue_tracked_time_test.go b/models/issues/tracked_time_test.go index a628329712..787ba9b701 100644 --- a/models/issue_tracked_time_test.go +++ b/models/issues/tracked_time_test.go @@ -2,13 +2,14 @@ // Use of this source code is governed by a MIT-style // license that can be found in the LICENSE file. -package models +package issues_test import ( "testing" "time" "code.gitea.io/gitea/models/db" + issues_model "code.gitea.io/gitea/models/issues" "code.gitea.io/gitea/models/unittest" user_model "code.gitea.io/gitea/models/user" @@ -21,20 +22,20 @@ func TestAddTime(t *testing.T) { user3, err := user_model.GetUserByID(3) assert.NoError(t, err) - issue1, err := GetIssueByID(1) + issue1, err := issues_model.GetIssueByID(db.DefaultContext, 1) assert.NoError(t, err) // 3661 = 1h 1min 1s - trackedTime, err := AddTime(user3, issue1, 3661, time.Now()) + trackedTime, err := issues_model.AddTime(user3, issue1, 3661, time.Now()) assert.NoError(t, err) assert.Equal(t, int64(3), trackedTime.UserID) assert.Equal(t, int64(1), trackedTime.IssueID) assert.Equal(t, int64(3661), trackedTime.Time) - tt := unittest.AssertExistsAndLoadBean(t, &TrackedTime{UserID: 3, IssueID: 1}).(*TrackedTime) + tt := unittest.AssertExistsAndLoadBean(t, &issues_model.TrackedTime{UserID: 3, IssueID: 1}).(*issues_model.TrackedTime) assert.Equal(t, int64(3661), tt.Time) - comment := unittest.AssertExistsAndLoadBean(t, &Comment{Type: CommentTypeAddTimeManual, PosterID: 3, IssueID: 1}).(*Comment) + comment := unittest.AssertExistsAndLoadBean(t, &issues_model.Comment{Type: issues_model.CommentTypeAddTimeManual, PosterID: 3, IssueID: 1}).(*issues_model.Comment) assert.Equal(t, comment.Content, "1 hour 1 minute") } @@ -42,39 +43,39 @@ func TestGetTrackedTimes(t *testing.T) { assert.NoError(t, unittest.PrepareTestDatabase()) // by Issue - times, err := GetTrackedTimes(db.DefaultContext, &FindTrackedTimesOptions{IssueID: 1}) + times, err := issues_model.GetTrackedTimes(db.DefaultContext, &issues_model.FindTrackedTimesOptions{IssueID: 1}) assert.NoError(t, err) assert.Len(t, times, 1) assert.Equal(t, int64(400), times[0].Time) - times, err = GetTrackedTimes(db.DefaultContext, &FindTrackedTimesOptions{IssueID: -1}) + times, err = issues_model.GetTrackedTimes(db.DefaultContext, &issues_model.FindTrackedTimesOptions{IssueID: -1}) assert.NoError(t, err) assert.Len(t, times, 0) // by User - times, err = GetTrackedTimes(db.DefaultContext, &FindTrackedTimesOptions{UserID: 1}) + times, err = issues_model.GetTrackedTimes(db.DefaultContext, &issues_model.FindTrackedTimesOptions{UserID: 1}) assert.NoError(t, err) assert.Len(t, times, 3) assert.Equal(t, int64(400), times[0].Time) - times, err = GetTrackedTimes(db.DefaultContext, &FindTrackedTimesOptions{UserID: 3}) + times, err = issues_model.GetTrackedTimes(db.DefaultContext, &issues_model.FindTrackedTimesOptions{UserID: 3}) assert.NoError(t, err) assert.Len(t, times, 0) // by Repo - times, err = GetTrackedTimes(db.DefaultContext, &FindTrackedTimesOptions{RepositoryID: 2}) + times, err = issues_model.GetTrackedTimes(db.DefaultContext, &issues_model.FindTrackedTimesOptions{RepositoryID: 2}) assert.NoError(t, err) assert.Len(t, times, 3) assert.Equal(t, int64(1), times[0].Time) - issue, err := GetIssueByID(times[0].IssueID) + issue, err := issues_model.GetIssueByID(db.DefaultContext, times[0].IssueID) assert.NoError(t, err) assert.Equal(t, issue.RepoID, int64(2)) - times, err = GetTrackedTimes(db.DefaultContext, &FindTrackedTimesOptions{RepositoryID: 1}) + times, err = issues_model.GetTrackedTimes(db.DefaultContext, &issues_model.FindTrackedTimesOptions{RepositoryID: 1}) assert.NoError(t, err) assert.Len(t, times, 5) - times, err = GetTrackedTimes(db.DefaultContext, &FindTrackedTimesOptions{RepositoryID: 10}) + times, err = issues_model.GetTrackedTimes(db.DefaultContext, &issues_model.FindTrackedTimesOptions{RepositoryID: 10}) assert.NoError(t, err) assert.Len(t, times, 0) } @@ -82,7 +83,7 @@ func TestGetTrackedTimes(t *testing.T) { func TestTotalTimes(t *testing.T) { assert.NoError(t, unittest.PrepareTestDatabase()) - total, err := TotalTimes(&FindTrackedTimesOptions{IssueID: 1}) + total, err := issues_model.TotalTimes(&issues_model.FindTrackedTimesOptions{IssueID: 1}) assert.NoError(t, err) assert.Len(t, total, 1) for user, time := range total { @@ -90,7 +91,7 @@ func TestTotalTimes(t *testing.T) { assert.Equal(t, "6 minutes 40 seconds", time) } - total, err = TotalTimes(&FindTrackedTimesOptions{IssueID: 2}) + total, err = issues_model.TotalTimes(&issues_model.FindTrackedTimesOptions{IssueID: 2}) assert.NoError(t, err) assert.Len(t, total, 2) for user, time := range total { @@ -103,7 +104,7 @@ func TestTotalTimes(t *testing.T) { } } - total, err = TotalTimes(&FindTrackedTimesOptions{IssueID: 5}) + total, err = issues_model.TotalTimes(&issues_model.FindTrackedTimesOptions{IssueID: 5}) assert.NoError(t, err) assert.Len(t, total, 1) for user, time := range total { @@ -111,7 +112,7 @@ func TestTotalTimes(t *testing.T) { assert.Equal(t, "1 second", time) } - total, err = TotalTimes(&FindTrackedTimesOptions{IssueID: 4}) + total, err = issues_model.TotalTimes(&issues_model.FindTrackedTimesOptions{IssueID: 4}) assert.NoError(t, err) assert.Len(t, total, 2) } diff --git a/models/main_test.go b/models/main_test.go index 96231e4704..bb2fedc15a 100644 --- a/models/main_test.go +++ b/models/main_test.go @@ -7,7 +7,6 @@ package models import ( "testing" - issues_model "code.gitea.io/gitea/models/issues" "code.gitea.io/gitea/models/organization" repo_model "code.gitea.io/gitea/models/repo" "code.gitea.io/gitea/models/unittest" @@ -28,10 +27,6 @@ func TestFixturesAreConsistent(t *testing.T) { unittest.CheckConsistencyFor(t, &user_model.User{}, &repo_model.Repository{}, - &Issue{}, - &PullRequest{}, - &issues_model.Milestone{}, - &Label{}, &organization.Team{}, &Action{}) } diff --git a/models/migrate.go b/models/migrate.go index 7b12bc9c93..0af3891cb8 100644 --- a/models/migrate.go +++ b/models/migrate.go @@ -10,8 +10,6 @@ import ( "code.gitea.io/gitea/models/db" issues_model "code.gitea.io/gitea/models/issues" "code.gitea.io/gitea/modules/structs" - - "xorm.io/builder" ) // InsertMilestones creates milestones of repository. @@ -41,7 +39,7 @@ func InsertMilestones(ms ...*issues_model.Milestone) (err error) { } // InsertIssues insert issues to database -func InsertIssues(issues ...*Issue) error { +func InsertIssues(issues ...*issues_model.Issue) error { ctx, committer, err := db.TxContext() if err != nil { return err @@ -56,14 +54,14 @@ func InsertIssues(issues ...*Issue) error { return committer.Commit() } -func insertIssue(ctx context.Context, issue *Issue) error { +func insertIssue(ctx context.Context, issue *issues_model.Issue) error { sess := db.GetEngine(ctx) if _, err := sess.NoAutoTime().Insert(issue); err != nil { return err } - issueLabels := make([]IssueLabel, 0, len(issue.Labels)) + issueLabels := make([]issues_model.IssueLabel, 0, len(issue.Labels)) for _, label := range issue.Labels { - issueLabels = append(issueLabels, IssueLabel{ + issueLabels = append(issueLabels, issues_model.IssueLabel{ IssueID: issue.ID, LabelID: label.ID, }) @@ -95,7 +93,7 @@ func insertIssue(ctx context.Context, issue *Issue) error { } // InsertIssueComments inserts many comments of issues. -func InsertIssueComments(comments []*Comment) error { +func InsertIssueComments(comments []*issues_model.Comment) error { if len(comments) == 0 { return nil } @@ -127,7 +125,8 @@ func InsertIssueComments(comments []*Comment) error { } for issueID := range issueIDs { - if _, err := db.Exec(ctx, "UPDATE issue set num_comments = (SELECT count(*) FROM comment WHERE issue_id = ? AND `type`=?) WHERE id = ?", issueID, CommentTypeComment, issueID); err != nil { + if _, err := db.Exec(ctx, "UPDATE issue set num_comments = (SELECT count(*) FROM comment WHERE issue_id = ? AND `type`=?) WHERE id = ?", + issueID, issues_model.CommentTypeComment, issueID); err != nil { return err } } @@ -135,7 +134,7 @@ func InsertIssueComments(comments []*Comment) error { } // InsertPullRequests inserted pull requests -func InsertPullRequests(prs ...*PullRequest) error { +func InsertPullRequests(prs ...*issues_model.PullRequest) error { ctx, committer, err := db.TxContext() if err != nil { return err @@ -182,37 +181,13 @@ func InsertReleases(rels ...*Release) error { return committer.Commit() } -func migratedIssueCond(tp structs.GitServiceType) builder.Cond { - return builder.In("issue_id", - builder.Select("issue.id"). - From("issue"). - InnerJoin("repository", "issue.repo_id = repository.id"). - Where(builder.Eq{ - "repository.original_service_type": tp, - }), - ) -} - -// UpdateReviewsMigrationsByType updates reviews' migrations information via given git service type and original id and poster id -func UpdateReviewsMigrationsByType(tp structs.GitServiceType, originalAuthorID string, posterID int64) error { - _, err := db.GetEngine(db.DefaultContext).Table("review"). - Where("original_author_id = ?", originalAuthorID). - And(migratedIssueCond(tp)). - Update(map[string]interface{}{ - "reviewer_id": posterID, - "original_author": "", - "original_author_id": 0, - }) - return err -} - // UpdateMigrationsByType updates all migrated repositories' posterid from gitServiceType to replace originalAuthorID to posterID func UpdateMigrationsByType(tp structs.GitServiceType, externalUserID string, userID int64) error { - if err := UpdateIssuesMigrationsByType(tp, externalUserID, userID); err != nil { + if err := issues_model.UpdateIssuesMigrationsByType(tp, externalUserID, userID); err != nil { return err } - if err := UpdateCommentsMigrationsByType(tp, externalUserID, userID); err != nil { + if err := issues_model.UpdateCommentsMigrationsByType(tp, externalUserID, userID); err != nil { return err } @@ -220,8 +195,8 @@ func UpdateMigrationsByType(tp structs.GitServiceType, externalUserID string, us return err } - if err := UpdateReactionsMigrationsByType(tp, externalUserID, userID); err != nil { + if err := issues_model.UpdateReactionsMigrationsByType(tp, externalUserID, userID); err != nil { return err } - return UpdateReviewsMigrationsByType(tp, externalUserID, userID) + return issues_model.UpdateReviewsMigrationsByType(tp, externalUserID, userID) } diff --git a/models/migrate_test.go b/models/migrate_test.go index ce28b3ca7c..b6525278ec 100644 --- a/models/migrate_test.go +++ b/models/migrate_test.go @@ -41,7 +41,7 @@ func assertCreateIssues(t *testing.T, isPull bool) { reponame := "repo1" repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{Name: reponame}).(*repo_model.Repository) owner := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: repo.OwnerID}).(*user_model.User) - label := unittest.AssertExistsAndLoadBean(t, &Label{ID: 1}).(*Label) + label := unittest.AssertExistsAndLoadBean(t, &issues_model.Label{ID: 1}).(*issues_model.Label) milestone := unittest.AssertExistsAndLoadBean(t, &issues_model.Milestone{ID: 1}).(*issues_model.Milestone) assert.EqualValues(t, milestone.ID, 1) reaction := &issues_model.Reaction{ @@ -51,7 +51,7 @@ func assertCreateIssues(t *testing.T, isPull bool) { foreignIndex := int64(12345) title := "issuetitle1" - is := &Issue{ + is := &issues_model.Issue{ RepoID: repo.ID, MilestoneID: milestone.ID, Repo: repo, @@ -61,7 +61,7 @@ func assertCreateIssues(t *testing.T, isPull bool) { PosterID: owner.ID, Poster: owner, IsClosed: true, - Labels: []*Label{label}, + Labels: []*issues_model.Label{label}, Reactions: []*issues_model.Reaction{reaction}, ForeignReference: &foreignreference.ForeignReference{ ForeignIndex: strconv.FormatInt(foreignIndex, 10), @@ -72,9 +72,9 @@ func assertCreateIssues(t *testing.T, isPull bool) { err := InsertIssues(is) assert.NoError(t, err) - i := unittest.AssertExistsAndLoadBean(t, &Issue{Title: title}).(*Issue) + i := unittest.AssertExistsAndLoadBean(t, &issues_model.Issue{Title: title}).(*issues_model.Issue) assert.Nil(t, i.ForeignReference) - err = i.LoadAttributes() + err = i.LoadAttributes(db.DefaultContext) assert.NoError(t, err) assert.EqualValues(t, strconv.FormatInt(foreignIndex, 10), i.ForeignReference.ForeignIndex) unittest.AssertExistsAndLoadBean(t, &issues_model.Reaction{Type: "heart", UserID: owner.ID, IssueID: i.ID}) @@ -90,7 +90,7 @@ func TestMigrate_CreateIssuesIsPullTrue(t *testing.T) { func TestMigrate_InsertIssueComments(t *testing.T) { assert.NoError(t, unittest.PrepareTestDatabase()) - issue := unittest.AssertExistsAndLoadBean(t, &Issue{ID: 1}).(*Issue) + issue := unittest.AssertExistsAndLoadBean(t, &issues_model.Issue{ID: 1}).(*issues_model.Issue) _ = issue.LoadRepo(db.DefaultContext) owner := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: issue.Repo.OwnerID}).(*user_model.User) reaction := &issues_model.Reaction{ @@ -98,7 +98,7 @@ func TestMigrate_InsertIssueComments(t *testing.T) { UserID: owner.ID, } - comment := &Comment{ + comment := &issues_model.Comment{ PosterID: owner.ID, Poster: owner, IssueID: issue.ID, @@ -106,13 +106,13 @@ func TestMigrate_InsertIssueComments(t *testing.T) { Reactions: []*issues_model.Reaction{reaction}, } - err := InsertIssueComments([]*Comment{comment}) + err := InsertIssueComments([]*issues_model.Comment{comment}) assert.NoError(t, err) - issueModified := unittest.AssertExistsAndLoadBean(t, &Issue{ID: 1}).(*Issue) + issueModified := unittest.AssertExistsAndLoadBean(t, &issues_model.Issue{ID: 1}).(*issues_model.Issue) assert.EqualValues(t, issue.NumComments+1, issueModified.NumComments) - unittest.CheckConsistencyFor(t, &Issue{}) + unittest.CheckConsistencyFor(t, &issues_model.Issue{}) } func TestMigrate_InsertPullRequests(t *testing.T) { @@ -121,7 +121,7 @@ func TestMigrate_InsertPullRequests(t *testing.T) { repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{Name: reponame}).(*repo_model.Repository) owner := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: repo.OwnerID}).(*user_model.User) - i := &Issue{ + i := &issues_model.Issue{ RepoID: repo.ID, Repo: repo, Title: "title1", @@ -131,16 +131,16 @@ func TestMigrate_InsertPullRequests(t *testing.T) { Poster: owner, } - p := &PullRequest{ + p := &issues_model.PullRequest{ Issue: i, } err := InsertPullRequests(p) assert.NoError(t, err) - _ = unittest.AssertExistsAndLoadBean(t, &PullRequest{IssueID: i.ID}).(*PullRequest) + _ = unittest.AssertExistsAndLoadBean(t, &issues_model.PullRequest{IssueID: i.ID}).(*issues_model.PullRequest) - unittest.CheckConsistencyFor(t, &Issue{}, &PullRequest{}) + unittest.CheckConsistencyFor(t, &issues_model.Issue{}, &issues_model.PullRequest{}) } func TestMigrate_InsertReleases(t *testing.T) { diff --git a/models/migrations/v111.go b/models/migrations/v111.go index 02624da66a..65fe7c5332 100644 --- a/models/migrations/v111.go +++ b/models/migrations/v111.go @@ -131,7 +131,7 @@ func addBranchProtectionCanPushAndEnableWhitelist(x *xorm.Engine) error { Authorize int } - // getUserRepoPermission static function based on models.IsOfficialReviewer at 5d78792385 + // getUserRepoPermission static function based on issues_model.IsOfficialReviewer at 5d78792385 getUserRepoPermission := func(sess *xorm.Session, repo *Repository, user *User) (Permission, error) { var perm Permission diff --git a/models/notification.go b/models/notification.go index ac5abc6f92..3f0e374b83 100644 --- a/models/notification.go +++ b/models/notification.go @@ -11,6 +11,7 @@ import ( "strconv" "code.gitea.io/gitea/models/db" + issues_model "code.gitea.io/gitea/models/issues" "code.gitea.io/gitea/models/organization" repo_model "code.gitea.io/gitea/models/repo" "code.gitea.io/gitea/models/unit" @@ -66,9 +67,9 @@ type Notification struct { UpdatedBy int64 `xorm:"INDEX NOT NULL"` - Issue *Issue `xorm:"-"` + Issue *issues_model.Issue `xorm:"-"` Repository *repo_model.Repository `xorm:"-"` - Comment *Comment `xorm:"-"` + Comment *issues_model.Comment `xorm:"-"` User *user_model.User `xorm:"-"` CreatedUnix timeutil.TimeStamp `xorm:"created INDEX NOT NULL"` @@ -204,7 +205,7 @@ func createOrUpdateIssueNotifications(ctx context.Context, issueID, commentID, n return err } - issue, err := getIssueByID(ctx, issueID) + issue, err := issues_model.GetIssueByID(ctx, issueID) if err != nil { return err } @@ -214,14 +215,14 @@ func createOrUpdateIssueNotifications(ctx context.Context, issueID, commentID, n toNotify[receiverID] = struct{}{} } else { toNotify = make(map[int64]struct{}, 32) - issueWatches, err := GetIssueWatchersIDs(ctx, issueID, true) + issueWatches, err := issues_model.GetIssueWatchersIDs(ctx, issueID, true) if err != nil { return err } for _, id := range issueWatches { toNotify[id] = struct{}{} } - if !(issue.IsPull && HasWorkInProgressPrefix(issue.Title)) { + if !(issue.IsPull && issues_model.HasWorkInProgressPrefix(issue.Title)) { repoWatches, err := repo_model.GetRepoWatchersIDs(ctx, issue.RepoID) if err != nil { return err @@ -230,7 +231,7 @@ func createOrUpdateIssueNotifications(ctx context.Context, issueID, commentID, n toNotify[id] = struct{}{} } } - issueParticipants, err := issue.getParticipantIDsByIssue(ctx) + issueParticipants, err := issue.GetParticipantIDsByIssue(ctx) if err != nil { return err } @@ -241,7 +242,7 @@ func createOrUpdateIssueNotifications(ctx context.Context, issueID, commentID, n // dont notify user who cause notification delete(toNotify, notificationAuthorID) // explicit unwatch on issue - issueUnWatches, err := GetIssueWatchersIDs(ctx, issueID, false) + issueUnWatches, err := issues_model.GetIssueWatchersIDs(ctx, issueID, false) if err != nil { return err } @@ -303,7 +304,7 @@ func notificationExists(notifications []*Notification, issueID, userID int64) bo return false } -func createIssueNotification(ctx context.Context, userID int64, issue *Issue, commentID, updatedByID int64) error { +func createIssueNotification(ctx context.Context, userID int64, issue *issues_model.Issue, commentID, updatedByID int64) error { notification := &Notification{ UserID: userID, RepoID: issue.RepoID, @@ -415,21 +416,21 @@ func (n *Notification) loadRepo(ctx context.Context) (err error) { func (n *Notification) loadIssue(ctx context.Context) (err error) { if n.Issue == nil && n.IssueID != 0 { - n.Issue, err = getIssueByID(ctx, n.IssueID) + n.Issue, err = issues_model.GetIssueByID(ctx, n.IssueID) if err != nil { return fmt.Errorf("getIssueByID [%d]: %v", n.IssueID, err) } - return n.Issue.loadAttributes(ctx) + return n.Issue.LoadAttributes(ctx) } return nil } func (n *Notification) loadComment(ctx context.Context) (err error) { if n.Comment == nil && n.CommentID != 0 { - n.Comment, err = GetCommentByID(ctx, n.CommentID) + n.Comment, err = issues_model.GetCommentByID(ctx, n.CommentID) if err != nil { - if IsErrCommentNotExist(err) { - return ErrCommentNotExist{ + if issues_model.IsErrCommentNotExist(err) { + return issues_model.ErrCommentNotExist{ ID: n.CommentID, IssueID: n.IssueID, } @@ -456,7 +457,7 @@ func (n *Notification) GetRepo() (*repo_model.Repository, error) { } // GetIssue returns the issue of the notification -func (n *Notification) GetIssue() (*Issue, error) { +func (n *Notification) GetIssue() (*issues_model.Issue, error) { return n.Issue, n.loadIssue(db.DefaultContext) } @@ -489,7 +490,7 @@ func (nl NotificationList) LoadAttributes() error { var err error for i := 0; i < len(nl); i++ { err = nl[i].LoadAttributes() - if err != nil && !IsErrCommentNotExist(err) { + if err != nil && !issues_model.IsErrCommentNotExist(err) { return err } } @@ -519,7 +520,7 @@ func (nl NotificationList) LoadRepos() (repo_model.RepositoryList, []int, error) repos := make(map[int64]*repo_model.Repository, len(repoIDs)) left := len(repoIDs) for left > 0 { - limit := defaultMaxInSize + limit := db.DefaultMaxInSize if left < limit { limit = left } @@ -592,22 +593,22 @@ func (nl NotificationList) LoadIssues() ([]int, error) { } issueIDs := nl.getPendingIssueIDs() - issues := make(map[int64]*Issue, len(issueIDs)) + issues := make(map[int64]*issues_model.Issue, len(issueIDs)) left := len(issueIDs) for left > 0 { - limit := defaultMaxInSize + limit := db.DefaultMaxInSize if left < limit { limit = left } rows, err := db.GetEngine(db.DefaultContext). In("id", issueIDs[:limit]). - Rows(new(Issue)) + Rows(new(issues_model.Issue)) if err != nil { return nil, err } for rows.Next() { - var issue Issue + var issue issues_model.Issue err = rows.Scan(&issue) if err != nil { rows.Close() @@ -678,22 +679,22 @@ func (nl NotificationList) LoadComments() ([]int, error) { } commentIDs := nl.getPendingCommentIDs() - comments := make(map[int64]*Comment, len(commentIDs)) + comments := make(map[int64]*issues_model.Comment, len(commentIDs)) left := len(commentIDs) for left > 0 { - limit := defaultMaxInSize + limit := db.DefaultMaxInSize if left < limit { limit = left } rows, err := db.GetEngine(db.DefaultContext). In("id", commentIDs[:limit]). - Rows(new(Comment)) + Rows(new(issues_model.Comment)) if err != nil { return nil, err } for rows.Next() { - var comment Comment + var comment issues_model.Comment err = rows.Scan(&comment) if err != nil { rows.Close() @@ -747,6 +748,15 @@ func GetUIDsAndNotificationCounts(since, until timeutil.TimeStamp) ([]UserIDCoun return res, db.GetEngine(db.DefaultContext).SQL(sql, since, until, NotificationStatusUnread).Find(&res) } +// SetIssueReadBy sets issue to be read by given user. +func SetIssueReadBy(ctx context.Context, issueID, userID int64) error { + if err := issues_model.UpdateIssueUserByRead(userID, issueID); err != nil { + return err + } + + return setIssueNotificationStatusReadIfUnread(ctx, userID, issueID) +} + func setIssueNotificationStatusReadIfUnread(ctx context.Context, userID, issueID int64) error { notification, err := getIssueNotification(ctx, userID, issueID) // ignore if not exists diff --git a/models/notification_test.go b/models/notification_test.go index 15c29389c8..16ff02d6c0 100644 --- a/models/notification_test.go +++ b/models/notification_test.go @@ -8,6 +8,7 @@ import ( "testing" "code.gitea.io/gitea/models/db" + issues_model "code.gitea.io/gitea/models/issues" "code.gitea.io/gitea/models/unittest" user_model "code.gitea.io/gitea/models/user" @@ -16,14 +17,14 @@ import ( func TestCreateOrUpdateIssueNotifications(t *testing.T) { assert.NoError(t, unittest.PrepareTestDatabase()) - issue := unittest.AssertExistsAndLoadBean(t, &Issue{ID: 1}).(*Issue) + issue := unittest.AssertExistsAndLoadBean(t, &issues_model.Issue{ID: 1}).(*issues_model.Issue) assert.NoError(t, CreateOrUpdateIssueNotifications(issue.ID, 0, 2, 0)) // User 9 is inactive, thus notifications for user 1 and 4 are created notf := unittest.AssertExistsAndLoadBean(t, &Notification{UserID: 1, IssueID: issue.ID}).(*Notification) assert.Equal(t, NotificationStatusUnread, notf.Status) - unittest.CheckConsistencyFor(t, &Issue{ID: issue.ID}) + unittest.CheckConsistencyFor(t, &issues_model.Issue{ID: issue.ID}) notf = unittest.AssertExistsAndLoadBean(t, &Notification{UserID: 4, IssueID: issue.ID}).(*Notification) assert.Equal(t, NotificationStatusUnread, notf.Status) diff --git a/models/org_team.go b/models/org_team.go index 7ff3095273..5d29e33337 100644 --- a/models/org_team.go +++ b/models/org_team.go @@ -13,6 +13,7 @@ import ( "code.gitea.io/gitea/models/db" git_model "code.gitea.io/gitea/models/git" + issues_model "code.gitea.io/gitea/models/issues" "code.gitea.io/gitea/models/organization" access_model "code.gitea.io/gitea/models/perm/access" repo_model "code.gitea.io/gitea/models/repo" @@ -153,7 +154,7 @@ func removeAllRepositories(ctx context.Context, t *organization.Team) (err error } // Remove all IssueWatches a user has subscribed to in the repositories - if err = removeIssueWatchersByRepoID(ctx, user.ID, repo.ID); err != nil { + if err = issues_model.RemoveIssueWatchersByRepoID(ctx, user.ID, repo.ID); err != nil { return err } } @@ -216,7 +217,7 @@ func removeRepository(ctx context.Context, t *organization.Team, repo *repo_mode } // Remove all IssueWatches a user has subscribed to in the repositories - if err := removeIssueWatchersByRepoID(ctx, teamUser.UID, repo.ID); err != nil { + if err := issues_model.RemoveIssueWatchersByRepoID(ctx, teamUser.UID, repo.ID); err != nil { return err } } diff --git a/models/repo.go b/models/repo.go index a8aa18381d..e9d83f5f32 100644 --- a/models/repo.go +++ b/models/repo.go @@ -281,7 +281,7 @@ func DeleteRepository(doer *user_model.User, uid, repoID int64) error { &access_model.Access{RepoID: repo.ID}, &Action{RepoID: repo.ID}, &repo_model.Collaboration{RepoID: repoID}, - &Comment{RefRepoID: repoID}, + &issues_model.Comment{RefRepoID: repoID}, &git_model.CommitStatus{RepoID: repoID}, &git_model.DeletedBranch{RepoID: repoID}, &webhook.HookTask{RepoID: repoID}, @@ -306,18 +306,18 @@ func DeleteRepository(doer *user_model.User, uid, repoID int64) error { } // Delete Labels and related objects - if err := deleteLabelsByRepoID(ctx, repoID); err != nil { + if err := issues_model.DeleteLabelsByRepoID(ctx, repoID); err != nil { return err } // Delete Pulls and related objects - if err := deletePullsByBaseRepoID(ctx, repoID); err != nil { + if err := issues_model.DeletePullsByBaseRepoID(ctx, repoID); err != nil { return err } // Delete Issues and related objects var attachmentPaths []string - if attachmentPaths, err = deleteIssuesByRepoID(ctx, repoID); err != nil { + if attachmentPaths, err = issues_model.DeleteIssuesByRepoID(ctx, repoID); err != nil { return err } @@ -576,16 +576,11 @@ func repoStatsCorrectNum(ctx context.Context, id int64, isPull bool, field strin } func repoStatsCorrectNumClosedIssues(ctx context.Context, id int64) error { - return repoStatsCorrectNumClosed(ctx, id, false, "num_closed_issues") + return repo_model.StatsCorrectNumClosed(ctx, id, false, "num_closed_issues") } func repoStatsCorrectNumClosedPulls(ctx context.Context, id int64) error { - return repoStatsCorrectNumClosed(ctx, id, true, "num_closed_pulls") -} - -func repoStatsCorrectNumClosed(ctx context.Context, id int64, isPull bool, field string) error { - _, err := db.GetEngine(ctx).Exec("UPDATE `repository` SET "+field+"=(SELECT COUNT(*) FROM `issue` WHERE repo_id=? AND is_closed=? AND is_pull=?) WHERE id=?", id, true, isPull, id) - return err + return repo_model.StatsCorrectNumClosed(ctx, id, true, "num_closed_pulls") } func statsQuery(args ...interface{}) func(context.Context) ([]map[string][]byte, error) { @@ -687,12 +682,11 @@ func CheckRepoStats(ctx context.Context) error { continue } - rawResult, err := e.Query("SELECT COUNT(*) FROM `repository` WHERE fork_id=?", repo.ID) + _, err = e.SQL("SELECT COUNT(*) FROM `repository` WHERE fork_id=?", repo.ID).Get(&repo.NumForks) if err != nil { log.Error("Select count of forks[%d]: %v", repo.ID, err) continue } - repo.NumForks = int(parseCountResult(rawResult)) if _, err = e.ID(repo.ID).Cols("num_forks").Update(repo); err != nil { log.Error("UpdateRepository[%d]: %v", id, err) diff --git a/models/repo/repo.go b/models/repo/repo.go index 57d85435eb..f6097d2d6a 100644 --- a/models/repo/repo.go +++ b/models/repo/repo.go @@ -25,6 +25,22 @@ import ( "code.gitea.io/gitea/modules/util" ) +// ErrUserDoesNotHaveAccessToRepo represets an error where the user doesn't has access to a given repo. +type ErrUserDoesNotHaveAccessToRepo struct { + UserID int64 + RepoName string +} + +// IsErrUserDoesNotHaveAccessToRepo checks if an error is a ErrRepoFileAlreadyExists. +func IsErrUserDoesNotHaveAccessToRepo(err error) bool { + _, ok := err.(ErrUserDoesNotHaveAccessToRepo) + return ok +} + +func (err ErrUserDoesNotHaveAccessToRepo) Error() string { + return fmt.Sprintf("user doesn't have access to repo [user_id: %d, repo_name: %s]", err.UserID, err.RepoName) +} + var ( reservedRepoNames = []string{".", "..", "-"} reservedRepoPatterns = []string{"*.git", "*.wiki", "*.rss", "*.atom"} @@ -743,3 +759,34 @@ func CountRepositories(ctx context.Context, opts CountRepositoryOptions) (int64, } return count, nil } + +// StatsCorrectNumClosed update repository's issue related numbers +func StatsCorrectNumClosed(ctx context.Context, id int64, isPull bool, field string) error { + _, err := db.Exec(ctx, "UPDATE `repository` SET "+field+"=(SELECT COUNT(*) FROM `issue` WHERE repo_id=? AND is_closed=? AND is_pull=?) WHERE id=?", id, true, isPull, id) + return err +} + +// UpdateRepoIssueNumbers update repository issue numbers +func UpdateRepoIssueNumbers(ctx context.Context, repoID int64, isPull, isClosed bool) error { + e := db.GetEngine(ctx) + if isPull { + if _, err := e.ID(repoID).Decr("num_pulls").Update(new(Repository)); err != nil { + return err + } + if isClosed { + if _, err := e.ID(repoID).Decr("num_closed_pulls").Update(new(Repository)); err != nil { + return err + } + } + } else { + if _, err := e.ID(repoID).Decr("num_issues").Update(new(Repository)); err != nil { + return err + } + if isClosed { + if _, err := e.ID(repoID).Decr("num_closed_issues").Update(new(Repository)); err != nil { + return err + } + } + } + return nil +} diff --git a/models/repo_activity.go b/models/repo_activity.go index 06710ff1ac..6a3636ab07 100644 --- a/models/repo_activity.go +++ b/models/repo_activity.go @@ -11,6 +11,7 @@ import ( "time" "code.gitea.io/gitea/models/db" + issues_model "code.gitea.io/gitea/models/issues" repo_model "code.gitea.io/gitea/models/repo" user_model "code.gitea.io/gitea/models/user" "code.gitea.io/gitea/modules/git" @@ -29,15 +30,15 @@ type ActivityAuthorData struct { // ActivityStats represets issue and pull request information. type ActivityStats struct { - OpenedPRs PullRequestList + OpenedPRs issues_model.PullRequestList OpenedPRAuthorCount int64 - MergedPRs PullRequestList + MergedPRs issues_model.PullRequestList MergedPRAuthorCount int64 - OpenedIssues IssueList + OpenedIssues issues_model.IssueList OpenedIssueAuthorCount int64 - ClosedIssues IssueList + ClosedIssues issues_model.IssueList ClosedIssueAuthorCount int64 - UnresolvedIssues IssueList + UnresolvedIssues issues_model.IssueList PublishedReleases []*Release PublishedReleaseAuthorCount int64 Code *git.CodeActivityStats @@ -212,7 +213,7 @@ func (stats *ActivityStats) FillPullRequests(repoID int64, fromTime time.Time) e // Merged pull requests sess := pullRequestsForActivityStatement(repoID, fromTime, true) sess.OrderBy("pull_request.merged_unix DESC") - stats.MergedPRs = make(PullRequestList, 0) + stats.MergedPRs = make(issues_model.PullRequestList, 0) if err = sess.Find(&stats.MergedPRs); err != nil { return err } @@ -230,7 +231,7 @@ func (stats *ActivityStats) FillPullRequests(repoID int64, fromTime time.Time) e // Opened pull requests sess = pullRequestsForActivityStatement(repoID, fromTime, false) sess.OrderBy("issue.created_unix ASC") - stats.OpenedPRs = make(PullRequestList, 0) + stats.OpenedPRs = make(issues_model.PullRequestList, 0) if err = sess.Find(&stats.OpenedPRs); err != nil { return err } @@ -271,7 +272,7 @@ func (stats *ActivityStats) FillIssues(repoID int64, fromTime time.Time) error { // Closed issues sess := issuesForActivityStatement(repoID, fromTime, true, false) sess.OrderBy("issue.closed_unix DESC") - stats.ClosedIssues = make(IssueList, 0) + stats.ClosedIssues = make(issues_model.IssueList, 0) if err = sess.Find(&stats.ClosedIssues); err != nil { return err } @@ -286,7 +287,7 @@ func (stats *ActivityStats) FillIssues(repoID int64, fromTime time.Time) error { // New issues sess = issuesForActivityStatement(repoID, fromTime, false, false) sess.OrderBy("issue.created_unix ASC") - stats.OpenedIssues = make(IssueList, 0) + stats.OpenedIssues = make(issues_model.IssueList, 0) if err = sess.Find(&stats.OpenedIssues); err != nil { return err } @@ -312,7 +313,7 @@ func (stats *ActivityStats) FillUnresolvedIssues(repoID int64, fromTime time.Tim sess.And("issue.is_pull = ?", prs) } sess.OrderBy("issue.updated_unix DESC") - stats.UnresolvedIssues = make(IssueList, 0) + stats.UnresolvedIssues = make(issues_model.IssueList, 0) return sess.Find(&stats.UnresolvedIssues) } diff --git a/models/repo_collaboration.go b/models/repo_collaboration.go index 7d43115b23..c8866421bd 100644 --- a/models/repo_collaboration.go +++ b/models/repo_collaboration.go @@ -10,6 +10,7 @@ import ( "fmt" "code.gitea.io/gitea/models/db" + issues_model "code.gitea.io/gitea/models/issues" "code.gitea.io/gitea/models/perm" access_model "code.gitea.io/gitea/models/perm/access" repo_model "code.gitea.io/gitea/models/repo" @@ -101,7 +102,7 @@ func reconsiderRepoIssuesAssignee(ctx context.Context, repo *repo_model.Reposito if _, err := db.GetEngine(ctx).Where(builder.Eq{"assignee_id": uid}). In("issue_id", builder.Select("id").From("issue").Where(builder.Eq{"repo_id": repo.ID})). - Delete(&IssueAssignees{}); err != nil { + Delete(&issues_model.IssueAssignees{}); err != nil { return fmt.Errorf("Could not delete assignee[%d] %v", uid, err) } return nil @@ -116,5 +117,5 @@ func reconsiderWatches(ctx context.Context, repo *repo_model.Repository, uid int } // Remove all IssueWatches a user has subscribed to in the repository - return removeIssueWatchersByRepoID(ctx, uid, repo.ID) + return issues_model.RemoveIssueWatchersByRepoID(ctx, uid, repo.ID) } diff --git a/models/repo_transfer.go b/models/repo_transfer.go index 79cfc699c8..7d07fb252c 100644 --- a/models/repo_transfer.go +++ b/models/repo_transfer.go @@ -10,6 +10,7 @@ import ( "os" "code.gitea.io/gitea/models/db" + issues_model "code.gitea.io/gitea/models/issues" "code.gitea.io/gitea/models/organization" access_model "code.gitea.io/gitea/models/perm/access" repo_model "code.gitea.io/gitea/models/repo" @@ -376,7 +377,7 @@ func TransferOwnership(doer *user_model.User, newOwnerName string, repo *repo_mo INNER JOIN issue ON issue.id = com.issue_id WHERE com.type = ? AND issue.repo_id = ? AND ((label.org_id = 0 AND issue.repo_id != label.repo_id) OR (label.repo_id = 0 AND label.org_id != ?)) - ) AS il_too)`, CommentTypeLabel, repo.ID, newOwner.ID); err != nil { + ) AS il_too)`, issues_model.CommentTypeLabel, repo.ID, newOwner.ID); err != nil { return fmt.Errorf("Unable to remove old org label comments: %v", err) } } diff --git a/models/statistic.go b/models/statistic.go index dfc236ec58..55ace626c8 100644 --- a/models/statistic.go +++ b/models/statistic.go @@ -97,7 +97,7 @@ func GetStatistic() (stats Statistic) { stats.Counter.Issue = stats.Counter.IssueClosed + stats.Counter.IssueOpen - stats.Counter.Comment, _ = e.Count(new(Comment)) + stats.Counter.Comment, _ = e.Count(new(issues_model.Comment)) stats.Counter.Oauth = 0 stats.Counter.Follow, _ = e.Count(new(user_model.Follow)) stats.Counter.Mirror, _ = e.Count(new(repo_model.Mirror)) @@ -105,7 +105,7 @@ func GetStatistic() (stats Statistic) { stats.Counter.AuthSource = auth.CountSources() stats.Counter.Webhook, _ = e.Count(new(webhook.Webhook)) stats.Counter.Milestone, _ = e.Count(new(issues_model.Milestone)) - stats.Counter.Label, _ = e.Count(new(Label)) + stats.Counter.Label, _ = e.Count(new(issues_model.Label)) stats.Counter.HookTask, _ = e.Count(new(webhook.HookTask)) stats.Counter.Team, _ = e.Count(new(organization.Team)) stats.Counter.Attachment, _ = e.Count(new(repo_model.Attachment)) diff --git a/models/user.go b/models/user.go index 59ec643d55..49374014aa 100644 --- a/models/user.go +++ b/models/user.go @@ -16,7 +16,7 @@ import ( auth_model "code.gitea.io/gitea/models/auth" "code.gitea.io/gitea/models/db" git_model "code.gitea.io/gitea/models/git" - "code.gitea.io/gitea/models/issues" + issues_model "code.gitea.io/gitea/models/issues" "code.gitea.io/gitea/models/organization" access_model "code.gitea.io/gitea/models/perm/access" pull_model "code.gitea.io/gitea/models/pull" @@ -78,12 +78,12 @@ func DeleteUser(ctx context.Context, u *user_model.User) (err error) { &user_model.Follow{UserID: u.ID}, &user_model.Follow{FollowID: u.ID}, &Action{UserID: u.ID}, - &IssueUser{UID: u.ID}, + &issues_model.IssueUser{UID: u.ID}, &user_model.EmailAddress{UID: u.ID}, &user_model.UserOpenID{UID: u.ID}, - &issues.Reaction{UserID: u.ID}, + &issues_model.Reaction{UserID: u.ID}, &organization.TeamUser{UID: u.ID}, - &Stopwatch{UserID: u.ID}, + &issues_model.Stopwatch{UserID: u.ID}, &user_model.Setting{UserID: u.ID}, &pull_model.AutoMerge{DoerID: u.ID}, &pull_model.ReviewState{UserID: u.ID}, @@ -101,8 +101,8 @@ func DeleteUser(ctx context.Context, u *user_model.User) (err error) { // Delete Comments const batchSize = 50 for start := 0; ; start += batchSize { - comments := make([]*Comment, 0, batchSize) - if err = e.Where("type=? AND poster_id=?", CommentTypeComment, u.ID).Limit(batchSize, start).Find(&comments); err != nil { + comments := make([]*issues_model.Comment, 0, batchSize) + if err = e.Where("type=? AND poster_id=?", issues_model.CommentTypeComment, u.ID).Limit(batchSize, start).Find(&comments); err != nil { return err } if len(comments) == 0 { @@ -110,14 +110,14 @@ func DeleteUser(ctx context.Context, u *user_model.User) (err error) { } for _, comment := range comments { - if err = deleteComment(ctx, comment); err != nil { + if err = issues_model.DeleteComment(ctx, comment); err != nil { return err } } } // Delete Reactions - if err = issues.DeleteReaction(ctx, &issues.ReactionOptions{DoerID: u.ID}); err != nil { + if err = issues_model.DeleteReaction(ctx, &issues_model.ReactionOptions{DoerID: u.ID}); err != nil { return err } } @@ -189,7 +189,7 @@ func DeleteUser(ctx context.Context, u *user_model.User) (err error) { // ***** END: GPGPublicKey ***** // Clear assignee. - if _, err = db.DeleteByBean(ctx, &IssueAssignees{AssigneeID: u.ID}); err != nil { + if _, err = db.DeleteByBean(ctx, &issues_model.IssueAssignees{AssigneeID: u.ID}); err != nil { return fmt.Errorf("clear assignee: %v", err) } |