From 1a9821f57a0293db3adc0eab8aff08ca5fa1026c Mon Sep 17 00:00:00 2001 From: Lunny Xiao Date: Mon, 13 Jun 2022 17:37:59 +0800 Subject: Move issues related files into models/issues (#19931) * Move access and repo permission to models/perm/access * fix test * fix git test * Move functions sequence * Some improvements per @KN4CK3R and @delvh * Move issues related code to models/issues * Move some issues related sub package * Merge * Fix test * Fix test * Fix test * Fix test * Rename some files --- integrations/api_comment_test.go | 50 +- integrations/api_issue_label_test.go | 22 +- integrations/api_issue_reaction_test.go | 6 +- integrations/api_issue_stopwatch_test.go | 12 +- integrations/api_issue_subscription_test.go | 14 +- integrations/api_issue_test.go | 17 +- integrations/api_issue_tracked_time_test.go | 18 +- integrations/api_pull_commits_test.go | 4 +- integrations/api_pull_review_test.go | 21 +- integrations/api_pull_test.go | 6 +- integrations/delete_user_test.go | 4 +- integrations/git_test.go | 26 +- integrations/issue_test.go | 39 +- integrations/pull_merge_test.go | 21 +- integrations/pull_update_test.go | 13 +- integrations/user_test.go | 6 +- models/action.go | 28 +- models/action_test.go | 43 + models/branch.go | 80 - models/consistency.go | 214 --- models/consistency_test.go | 149 -- models/db/consistency.go | 27 + models/db/index.go | 23 +- models/db/list_options.go | 5 + models/error.go | 376 ---- models/git/branches_test.go | 6 +- models/git/main_test.go | 1 + models/issue.go | 2479 --------------------------- models/issue_assignees.go | 171 -- models/issue_assignees_test.go | 91 - models/issue_comment.go | 1502 ---------------- models/issue_comment_list.go | 554 ------ models/issue_comment_test.go | 64 - models/issue_dependency.go | 131 -- models/issue_dependency_test.go | 62 - models/issue_label.go | 684 -------- models/issue_label_test.go | 394 ----- models/issue_list.go | 571 ------ models/issue_list_test.go | 72 - models/issue_lock.go | 65 - models/issue_project.go | 179 -- models/issue_stopwatch.go | 293 ---- models/issue_stopwatch_test.go | 78 - models/issue_test.go | 614 ------- models/issue_tracked_time.go | 316 ---- models/issue_tracked_time_test.go | 117 -- models/issue_user.go | 86 - models/issue_user_test.go | 61 - models/issue_watch.go | 134 -- models/issue_watch_test.go | 67 - models/issue_xref.go | 371 ---- models/issue_xref_test.go | 183 -- models/issues/assignees.go | 171 ++ models/issues/assignees_test.go | 92 + models/issues/comment.go | 1546 +++++++++++++++++ models/issues/comment_list.go | 553 ++++++ models/issues/comment_test.go | 65 + models/issues/content_history.go | 6 +- models/issues/content_history_test.go | 43 +- models/issues/dependency.go | 210 +++ models/issues/dependency_test.go | 63 + models/issues/issue.go | 2448 ++++++++++++++++++++++++++ models/issues/issue_index.go | 32 + models/issues/issue_list.go | 565 ++++++ models/issues/issue_list_test.go | 73 + models/issues/issue_lock.go | 65 + models/issues/issue_project.go | 179 ++ models/issues/issue_test.go | 562 ++++++ models/issues/issue_user.go | 87 + models/issues/issue_user_test.go | 62 + models/issues/issue_watch.go | 135 ++ models/issues/issue_watch_test.go | 68 + models/issues/issue_xref.go | 357 ++++ models/issues/issue_xref_test.go | 184 ++ models/issues/label.go | 836 +++++++++ models/issues/label_test.go | 395 +++++ models/issues/main_test.go | 24 +- models/issues/milestone.go | 33 +- models/issues/milestone_test.go | 151 +- models/issues/pull.go | 838 +++++++++ models/issues/pull_list.go | 216 +++ models/issues/pull_test.go | 277 +++ models/issues/reaction_test.go | 31 +- models/issues/review.go | 1018 +++++++++++ models/issues/review_test.go | 203 +++ models/issues/stopwatch.go | 293 ++++ models/issues/stopwatch_test.go | 79 + models/issues/tracked_time.go | 316 ++++ models/issues/tracked_time_test.go | 118 ++ models/main_test.go | 5 - models/migrate.go | 49 +- models/migrate_test.go | 28 +- models/migrations/v111.go | 2 +- models/notification.go | 58 +- models/notification_test.go | 5 +- models/org_team.go | 5 +- models/pull.go | 693 -------- models/pull_list.go | 216 --- models/pull_test.go | 255 --- models/repo.go | 20 +- models/repo/repo.go | 47 + models/repo_activity.go | 21 +- models/repo_collaboration.go | 5 +- models/repo_transfer.go | 3 +- models/review.go | 969 ----------- models/review_test.go | 202 --- models/statistic.go | 4 +- models/user.go | 18 +- modules/context/repo.go | 7 +- modules/convert/convert.go | 4 +- modules/convert/issue.go | 21 +- modules/convert/issue_comment.go | 14 +- modules/convert/issue_test.go | 3 +- modules/convert/pull.go | 8 +- modules/convert/pull_review.go | 20 +- modules/convert/pull_test.go | 6 +- modules/doctor/dbconsistency.go | 29 +- modules/doctor/mergebase.go | 10 +- modules/eventsource/manager_run.go | 3 +- modules/indexer/issues/db.go | 4 +- modules/indexer/issues/indexer.go | 12 +- modules/notification/action/action.go | 21 +- modules/notification/base/notifier.go | 47 +- modules/notification/base/null.go | 47 +- modules/notification/indexer/indexer.go | 24 +- modules/notification/mail/mail.go | 43 +- modules/notification/notification.go | 47 +- modules/notification/ui/ui.go | 29 +- modules/notification/webhook/webhook.go | 49 +- modules/repository/init.go | 9 +- modules/templates/helper.go | 3 +- routers/api/v1/misc/nodeinfo.go | 6 +- routers/api/v1/notify/threads.go | 5 +- routers/api/v1/org/label.go | 30 +- routers/api/v1/repo/file.go | 6 +- routers/api/v1/repo/issue.go | 59 +- routers/api/v1/repo/issue_comment.go | 66 +- routers/api/v1/repo/issue_label.go | 32 +- routers/api/v1/repo/issue_reaction.go | 17 +- routers/api/v1/repo/issue_stopwatch.go | 20 +- routers/api/v1/repo/issue_subscription.go | 24 +- routers/api/v1/repo/issue_tracked_time.go | 48 +- routers/api/v1/repo/label.go | 30 +- routers/api/v1/repo/patch.go | 3 +- routers/api/v1/repo/pull.go | 82 +- routers/api/v1/repo/pull_review.go | 64 +- routers/private/hook_post_receive.go | 10 +- routers/private/hook_pre_receive.go | 5 +- routers/web/org/org_labels.go | 16 +- routers/web/repo/branch.go | 5 +- routers/web/repo/compare.go | 5 +- routers/web/repo/issue.go | 276 +-- routers/web/repo/issue_content_history.go | 27 +- routers/web/repo/issue_dependency.go | 26 +- routers/web/repo/issue_label.go | 24 +- routers/web/repo/issue_label_test.go | 24 +- routers/web/repo/issue_lock.go | 6 +- routers/web/repo/issue_stopwatch.go | 14 +- routers/web/repo/issue_test.go | 158 +- routers/web/repo/issue_timetrack.go | 8 +- routers/web/repo/issue_watch.go | 4 +- routers/web/repo/projects.go | 14 +- routers/web/repo/pull.go | 65 +- routers/web/repo/pull_review.go | 24 +- routers/web/user/home.go | 52 +- routers/web/user/stop_watch.go | 6 +- services/agit/agit.go | 21 +- services/asymkey/sign.go | 6 +- services/automerge/automerge.go | 24 +- services/comments/comments.go | 36 +- services/forms/repo_form.go | 15 +- services/forms/user_form_hidden_comments.go | 58 +- services/gitdiff/gitdiff.go | 14 +- services/gitdiff/gitdiff_test.go | 10 +- services/gitdiff/main_test.go | 1 + services/issue/assignee.go | 56 +- services/issue/assignee_test.go | 6 +- services/issue/commit.go | 15 +- services/issue/commit_test.go | 57 +- services/issue/content.go | 6 +- services/issue/issue.go | 108 +- services/issue/issue_test.go | 61 +- services/issue/label.go | 32 +- services/issue/label_test.go | 16 +- services/issue/milestone.go | 13 +- services/issue/milestone_test.go | 9 +- services/issue/status.go | 14 +- services/mailer/mail.go | 29 +- services/mailer/mail_comment.go | 7 +- services/mailer/mail_issue.go | 17 +- services/mailer/mail_test.go | 17 +- services/migrations/gitea_uploader.go | 82 +- services/migrations/gitea_uploader_test.go | 6 +- services/pull/check.go | 31 +- services/pull/check_test.go | 12 +- services/pull/commit_status.go | 12 +- services/pull/edits.go | 6 +- services/pull/lfs.go | 6 +- services/pull/merge.go | 29 +- services/pull/patch.go | 21 +- services/pull/pull.go | 89 +- services/pull/pull_test.go | 6 +- services/pull/review.go | 60 +- services/pull/temp_repo.go | 5 +- services/pull/update.go | 17 +- services/repository/repository.go | 3 +- services/repository/template.go | 7 +- 207 files changed, 14160 insertions(+), 14170 deletions(-) delete mode 100644 models/branch.go delete mode 100644 models/consistency_test.go create mode 100644 models/db/consistency.go delete mode 100644 models/issue.go delete mode 100644 models/issue_assignees.go delete mode 100644 models/issue_assignees_test.go delete mode 100644 models/issue_comment.go delete mode 100644 models/issue_comment_list.go delete mode 100644 models/issue_comment_test.go delete mode 100644 models/issue_dependency.go delete mode 100644 models/issue_dependency_test.go delete mode 100644 models/issue_label.go delete mode 100644 models/issue_label_test.go delete mode 100644 models/issue_list.go delete mode 100644 models/issue_list_test.go delete mode 100644 models/issue_lock.go delete mode 100644 models/issue_project.go delete mode 100644 models/issue_stopwatch.go delete mode 100644 models/issue_stopwatch_test.go delete mode 100644 models/issue_test.go delete mode 100644 models/issue_tracked_time.go delete mode 100644 models/issue_tracked_time_test.go delete mode 100644 models/issue_user.go delete mode 100644 models/issue_user_test.go delete mode 100644 models/issue_watch.go delete mode 100644 models/issue_watch_test.go delete mode 100644 models/issue_xref.go delete mode 100644 models/issue_xref_test.go create mode 100644 models/issues/assignees.go create mode 100644 models/issues/assignees_test.go create mode 100644 models/issues/comment.go create mode 100644 models/issues/comment_list.go create mode 100644 models/issues/comment_test.go create mode 100644 models/issues/dependency.go create mode 100644 models/issues/dependency_test.go create mode 100644 models/issues/issue.go create mode 100644 models/issues/issue_index.go create mode 100644 models/issues/issue_list.go create mode 100644 models/issues/issue_list_test.go create mode 100644 models/issues/issue_lock.go create mode 100644 models/issues/issue_project.go create mode 100644 models/issues/issue_test.go create mode 100644 models/issues/issue_user.go create mode 100644 models/issues/issue_user_test.go create mode 100644 models/issues/issue_watch.go create mode 100644 models/issues/issue_watch_test.go create mode 100644 models/issues/issue_xref.go create mode 100644 models/issues/issue_xref_test.go create mode 100644 models/issues/label.go create mode 100644 models/issues/label_test.go create mode 100644 models/issues/pull.go create mode 100644 models/issues/pull_list.go create mode 100644 models/issues/pull_test.go create mode 100644 models/issues/review.go create mode 100644 models/issues/review_test.go create mode 100644 models/issues/stopwatch.go create mode 100644 models/issues/stopwatch_test.go create mode 100644 models/issues/tracked_time.go create mode 100644 models/issues/tracked_time_test.go delete mode 100644 models/pull.go delete mode 100644 models/pull_list.go delete mode 100644 models/pull_test.go delete mode 100644 models/review.go delete mode 100644 models/review_test.go diff --git a/integrations/api_comment_test.go b/integrations/api_comment_test.go index dde51b2d53..7dcc0279fc 100644 --- a/integrations/api_comment_test.go +++ b/integrations/api_comment_test.go @@ -10,7 +10,7 @@ import ( "net/url" "testing" - "code.gitea.io/gitea/models" + 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" @@ -23,9 +23,9 @@ import ( func TestAPIListRepoComments(t *testing.T) { defer prepareTestEnv(t)() - comment := unittest.AssertExistsAndLoadBean(t, &models.Comment{}, - unittest.Cond("type = ?", models.CommentTypeComment)).(*models.Comment) - issue := unittest.AssertExistsAndLoadBean(t, &models.Issue{ID: comment.IssueID}).(*models.Issue) + comment := unittest.AssertExistsAndLoadBean(t, &issues_model.Comment{}, + unittest.Cond("type = ?", issues_model.CommentTypeComment)).(*issues_model.Comment) + issue := unittest.AssertExistsAndLoadBean(t, &issues_model.Issue{ID: comment.IssueID}).(*issues_model.Issue) repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: issue.RepoID}).(*repo_model.Repository) repoOwner := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: repo.OwnerID}).(*user_model.User) @@ -38,10 +38,10 @@ func TestAPIListRepoComments(t *testing.T) { DecodeJSON(t, resp, &apiComments) assert.Len(t, apiComments, 2) for _, apiComment := range apiComments { - c := &models.Comment{ID: apiComment.ID} + c := &issues_model.Comment{ID: apiComment.ID} unittest.AssertExistsAndLoadBean(t, c, - unittest.Cond("type = ?", models.CommentTypeComment)) - unittest.AssertExistsAndLoadBean(t, &models.Issue{ID: c.IssueID, RepoID: repo.ID}) + unittest.Cond("type = ?", issues_model.CommentTypeComment)) + unittest.AssertExistsAndLoadBean(t, &issues_model.Issue{ID: c.IssueID, RepoID: repo.ID}) } // test before and since filters @@ -69,9 +69,9 @@ func TestAPIListRepoComments(t *testing.T) { func TestAPIListIssueComments(t *testing.T) { defer prepareTestEnv(t)() - comment := unittest.AssertExistsAndLoadBean(t, &models.Comment{}, - unittest.Cond("type = ?", models.CommentTypeComment)).(*models.Comment) - issue := unittest.AssertExistsAndLoadBean(t, &models.Issue{ID: comment.IssueID}).(*models.Issue) + comment := unittest.AssertExistsAndLoadBean(t, &issues_model.Comment{}, + unittest.Cond("type = ?", issues_model.CommentTypeComment)).(*issues_model.Comment) + issue := unittest.AssertExistsAndLoadBean(t, &issues_model.Issue{ID: comment.IssueID}).(*issues_model.Issue) repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: issue.RepoID}).(*repo_model.Repository) repoOwner := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: repo.OwnerID}).(*user_model.User) @@ -82,8 +82,8 @@ func TestAPIListIssueComments(t *testing.T) { var comments []*api.Comment DecodeJSON(t, resp, &comments) - expectedCount := unittest.GetCount(t, &models.Comment{IssueID: issue.ID}, - unittest.Cond("type = ?", models.CommentTypeComment)) + expectedCount := unittest.GetCount(t, &issues_model.Comment{IssueID: issue.ID}, + unittest.Cond("type = ?", issues_model.CommentTypeComment)) assert.EqualValues(t, expectedCount, len(comments)) } @@ -91,7 +91,7 @@ func TestAPICreateComment(t *testing.T) { defer prepareTestEnv(t)() const commentBody = "Comment body" - issue := unittest.AssertExistsAndLoadBean(t, &models.Issue{}).(*models.Issue) + issue := unittest.AssertExistsAndLoadBean(t, &issues_model.Issue{}).(*issues_model.Issue) repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: issue.RepoID}).(*repo_model.Repository) repoOwner := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: repo.OwnerID}).(*user_model.User) @@ -107,13 +107,13 @@ func TestAPICreateComment(t *testing.T) { var updatedComment api.Comment DecodeJSON(t, resp, &updatedComment) assert.EqualValues(t, commentBody, updatedComment.Body) - unittest.AssertExistsAndLoadBean(t, &models.Comment{ID: updatedComment.ID, IssueID: issue.ID, Content: commentBody}) + unittest.AssertExistsAndLoadBean(t, &issues_model.Comment{ID: updatedComment.ID, IssueID: issue.ID, Content: commentBody}) } func TestAPIGetComment(t *testing.T) { defer prepareTestEnv(t)() - comment := unittest.AssertExistsAndLoadBean(t, &models.Comment{ID: 2}).(*models.Comment) + comment := unittest.AssertExistsAndLoadBean(t, &issues_model.Comment{ID: 2}).(*issues_model.Comment) assert.NoError(t, comment.LoadIssue()) repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: comment.Issue.RepoID}).(*repo_model.Repository) repoOwner := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: repo.OwnerID}).(*user_model.User) @@ -141,9 +141,9 @@ func TestAPIEditComment(t *testing.T) { defer prepareTestEnv(t)() const newCommentBody = "This is the new comment body" - comment := unittest.AssertExistsAndLoadBean(t, &models.Comment{}, - unittest.Cond("type = ?", models.CommentTypeComment)).(*models.Comment) - issue := unittest.AssertExistsAndLoadBean(t, &models.Issue{ID: comment.IssueID}).(*models.Issue) + comment := unittest.AssertExistsAndLoadBean(t, &issues_model.Comment{}, + unittest.Cond("type = ?", issues_model.CommentTypeComment)).(*issues_model.Comment) + issue := unittest.AssertExistsAndLoadBean(t, &issues_model.Issue{ID: comment.IssueID}).(*issues_model.Issue) repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: issue.RepoID}).(*repo_model.Repository) repoOwner := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: repo.OwnerID}).(*user_model.User) @@ -160,15 +160,15 @@ func TestAPIEditComment(t *testing.T) { DecodeJSON(t, resp, &updatedComment) assert.EqualValues(t, comment.ID, updatedComment.ID) assert.EqualValues(t, newCommentBody, updatedComment.Body) - unittest.AssertExistsAndLoadBean(t, &models.Comment{ID: comment.ID, IssueID: issue.ID, Content: newCommentBody}) + unittest.AssertExistsAndLoadBean(t, &issues_model.Comment{ID: comment.ID, IssueID: issue.ID, Content: newCommentBody}) } func TestAPIDeleteComment(t *testing.T) { defer prepareTestEnv(t)() - comment := unittest.AssertExistsAndLoadBean(t, &models.Comment{}, - unittest.Cond("type = ?", models.CommentTypeComment)).(*models.Comment) - issue := unittest.AssertExistsAndLoadBean(t, &models.Issue{ID: comment.IssueID}).(*models.Issue) + comment := unittest.AssertExistsAndLoadBean(t, &issues_model.Comment{}, + unittest.Cond("type = ?", issues_model.CommentTypeComment)).(*issues_model.Comment) + issue := unittest.AssertExistsAndLoadBean(t, &issues_model.Issue{ID: comment.IssueID}).(*issues_model.Issue) repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: issue.RepoID}).(*repo_model.Repository) repoOwner := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: repo.OwnerID}).(*user_model.User) @@ -178,14 +178,14 @@ func TestAPIDeleteComment(t *testing.T) { repoOwner.Name, repo.Name, comment.ID, token) session.MakeRequest(t, req, http.StatusNoContent) - unittest.AssertNotExistsBean(t, &models.Comment{ID: comment.ID}) + unittest.AssertNotExistsBean(t, &issues_model.Comment{ID: comment.ID}) } func TestAPIListIssueTimeline(t *testing.T) { defer prepareTestEnv(t)() // load comment - issue := unittest.AssertExistsAndLoadBean(t, &models.Issue{ID: 1}).(*models.Issue) + issue := unittest.AssertExistsAndLoadBean(t, &issues_model.Issue{ID: 1}).(*issues_model.Issue) repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: issue.RepoID}).(*repo_model.Repository) repoOwner := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: repo.OwnerID}).(*user_model.User) @@ -199,6 +199,6 @@ func TestAPIListIssueTimeline(t *testing.T) { // lists extracted directly from DB are the same var comments []*api.TimelineComment DecodeJSON(t, resp, &comments) - expectedCount := unittest.GetCount(t, &models.Comment{IssueID: issue.ID}) + expectedCount := unittest.GetCount(t, &issues_model.Comment{IssueID: issue.ID}) assert.EqualValues(t, expectedCount, len(comments)) } diff --git a/integrations/api_issue_label_test.go b/integrations/api_issue_label_test.go index 94b487377e..9b6333b2a2 100644 --- a/integrations/api_issue_label_test.go +++ b/integrations/api_issue_label_test.go @@ -10,7 +10,7 @@ import ( "strings" "testing" - "code.gitea.io/gitea/models" + 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" @@ -37,7 +37,7 @@ func TestAPIModifyLabels(t *testing.T) { resp := session.MakeRequest(t, req, http.StatusCreated) apiLabel := new(api.Label) DecodeJSON(t, resp, &apiLabel) - dbLabel := unittest.AssertExistsAndLoadBean(t, &models.Label{ID: apiLabel.ID, RepoID: repo.ID}).(*models.Label) + dbLabel := unittest.AssertExistsAndLoadBean(t, &issues_model.Label{ID: apiLabel.ID, RepoID: repo.ID}).(*issues_model.Label) assert.EqualValues(t, dbLabel.Name, apiLabel.Name) assert.EqualValues(t, strings.TrimLeft(dbLabel.Color, "#"), apiLabel.Color) @@ -92,8 +92,8 @@ func TestAPIAddIssueLabels(t *testing.T) { assert.NoError(t, unittest.LoadFixtures()) repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 1}).(*repo_model.Repository) - issue := unittest.AssertExistsAndLoadBean(t, &models.Issue{RepoID: repo.ID}).(*models.Issue) - _ = unittest.AssertExistsAndLoadBean(t, &models.Label{RepoID: repo.ID, ID: 2}).(*models.Label) + issue := unittest.AssertExistsAndLoadBean(t, &issues_model.Issue{RepoID: repo.ID}).(*issues_model.Issue) + _ = unittest.AssertExistsAndLoadBean(t, &issues_model.Label{RepoID: repo.ID, ID: 2}).(*issues_model.Label) owner := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: repo.OwnerID}).(*user_model.User) session := loginUser(t, owner.Name) @@ -106,17 +106,17 @@ func TestAPIAddIssueLabels(t *testing.T) { resp := session.MakeRequest(t, req, http.StatusOK) var apiLabels []*api.Label DecodeJSON(t, resp, &apiLabels) - assert.Len(t, apiLabels, unittest.GetCount(t, &models.IssueLabel{IssueID: issue.ID})) + assert.Len(t, apiLabels, unittest.GetCount(t, &issues_model.IssueLabel{IssueID: issue.ID})) - unittest.AssertExistsAndLoadBean(t, &models.IssueLabel{IssueID: issue.ID, LabelID: 2}) + unittest.AssertExistsAndLoadBean(t, &issues_model.IssueLabel{IssueID: issue.ID, LabelID: 2}) } func TestAPIReplaceIssueLabels(t *testing.T) { assert.NoError(t, unittest.LoadFixtures()) repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 1}).(*repo_model.Repository) - issue := unittest.AssertExistsAndLoadBean(t, &models.Issue{RepoID: repo.ID}).(*models.Issue) - label := unittest.AssertExistsAndLoadBean(t, &models.Label{RepoID: repo.ID}).(*models.Label) + issue := unittest.AssertExistsAndLoadBean(t, &issues_model.Issue{RepoID: repo.ID}).(*issues_model.Issue) + label := unittest.AssertExistsAndLoadBean(t, &issues_model.Label{RepoID: repo.ID}).(*issues_model.Label) owner := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: repo.OwnerID}).(*user_model.User) session := loginUser(t, owner.Name) @@ -133,8 +133,8 @@ func TestAPIReplaceIssueLabels(t *testing.T) { assert.EqualValues(t, label.ID, apiLabels[0].ID) } - unittest.AssertCount(t, &models.IssueLabel{IssueID: issue.ID}, 1) - unittest.AssertExistsAndLoadBean(t, &models.IssueLabel{IssueID: issue.ID, LabelID: label.ID}) + unittest.AssertCount(t, &issues_model.IssueLabel{IssueID: issue.ID}, 1) + unittest.AssertExistsAndLoadBean(t, &issues_model.IssueLabel{IssueID: issue.ID, LabelID: label.ID}) } func TestAPIModifyOrgLabels(t *testing.T) { @@ -156,7 +156,7 @@ func TestAPIModifyOrgLabels(t *testing.T) { resp := session.MakeRequest(t, req, http.StatusCreated) apiLabel := new(api.Label) DecodeJSON(t, resp, &apiLabel) - dbLabel := unittest.AssertExistsAndLoadBean(t, &models.Label{ID: apiLabel.ID, OrgID: owner.ID}).(*models.Label) + dbLabel := unittest.AssertExistsAndLoadBean(t, &issues_model.Label{ID: apiLabel.ID, OrgID: owner.ID}).(*issues_model.Label) assert.EqualValues(t, dbLabel.Name, apiLabel.Name) assert.EqualValues(t, strings.TrimLeft(dbLabel.Color, "#"), apiLabel.Color) diff --git a/integrations/api_issue_reaction_test.go b/integrations/api_issue_reaction_test.go index 4a063c8c68..3834af2130 100644 --- a/integrations/api_issue_reaction_test.go +++ b/integrations/api_issue_reaction_test.go @@ -10,8 +10,8 @@ import ( "testing" "time" - "code.gitea.io/gitea/models" "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/convert" @@ -23,7 +23,7 @@ import ( func TestAPIIssuesReactions(t *testing.T) { defer prepareTestEnv(t)() - issue := unittest.AssertExistsAndLoadBean(t, &models.Issue{ID: 1}).(*models.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) @@ -80,7 +80,7 @@ func TestAPIIssuesReactions(t *testing.T) { func TestAPICommentReactions(t *testing.T) { defer prepareTestEnv(t)() - comment := unittest.AssertExistsAndLoadBean(t, &models.Comment{ID: 2}).(*models.Comment) + comment := unittest.AssertExistsAndLoadBean(t, &issues_model.Comment{ID: 2}).(*issues_model.Comment) _ = comment.LoadIssue() issue := comment.Issue _ = issue.LoadRepo(db.DefaultContext) diff --git a/integrations/api_issue_stopwatch_test.go b/integrations/api_issue_stopwatch_test.go index 90098b9236..0d06447181 100644 --- a/integrations/api_issue_stopwatch_test.go +++ b/integrations/api_issue_stopwatch_test.go @@ -8,8 +8,8 @@ import ( "net/http" "testing" - "code.gitea.io/gitea/models" "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" @@ -30,8 +30,8 @@ func TestAPIListStopWatches(t *testing.T) { resp := session.MakeRequest(t, req, http.StatusOK) var apiWatches []*api.StopWatch DecodeJSON(t, resp, &apiWatches) - stopwatch := unittest.AssertExistsAndLoadBean(t, &models.Stopwatch{UserID: owner.ID}).(*models.Stopwatch) - issue := unittest.AssertExistsAndLoadBean(t, &models.Issue{ID: stopwatch.IssueID}).(*models.Issue) + stopwatch := unittest.AssertExistsAndLoadBean(t, &issues_model.Stopwatch{UserID: owner.ID}).(*issues_model.Stopwatch) + issue := unittest.AssertExistsAndLoadBean(t, &issues_model.Issue{ID: stopwatch.IssueID}).(*issues_model.Issue) if assert.Len(t, apiWatches, 1) { assert.EqualValues(t, stopwatch.CreatedUnix.AsTime().Unix(), apiWatches[0].Created.Unix()) assert.EqualValues(t, issue.Index, apiWatches[0].IssueIndex) @@ -45,7 +45,7 @@ func TestAPIListStopWatches(t *testing.T) { func TestAPIStopStopWatches(t *testing.T) { defer prepareTestEnv(t)() - issue := unittest.AssertExistsAndLoadBean(t, &models.Issue{ID: 2}).(*models.Issue) + issue := unittest.AssertExistsAndLoadBean(t, &issues_model.Issue{ID: 2}).(*issues_model.Issue) _ = issue.LoadRepo(db.DefaultContext) owner := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: issue.Repo.OwnerID}).(*user_model.User) user := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2}).(*user_model.User) @@ -61,7 +61,7 @@ func TestAPIStopStopWatches(t *testing.T) { func TestAPICancelStopWatches(t *testing.T) { defer prepareTestEnv(t)() - issue := unittest.AssertExistsAndLoadBean(t, &models.Issue{ID: 1}).(*models.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) user := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 1}).(*user_model.User) @@ -77,7 +77,7 @@ func TestAPICancelStopWatches(t *testing.T) { func TestAPIStartStopWatches(t *testing.T) { defer prepareTestEnv(t)() - issue := unittest.AssertExistsAndLoadBean(t, &models.Issue{ID: 3}).(*models.Issue) + issue := unittest.AssertExistsAndLoadBean(t, &issues_model.Issue{ID: 3}).(*issues_model.Issue) _ = issue.LoadRepo(db.DefaultContext) owner := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: issue.Repo.OwnerID}).(*user_model.User) user := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2}).(*user_model.User) diff --git a/integrations/api_issue_subscription_test.go b/integrations/api_issue_subscription_test.go index e0bb388365..2c6cddcab9 100644 --- a/integrations/api_issue_subscription_test.go +++ b/integrations/api_issue_subscription_test.go @@ -9,7 +9,7 @@ import ( "net/http" "testing" - "code.gitea.io/gitea/models" + 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" @@ -21,18 +21,18 @@ import ( func TestAPIIssueSubscriptions(t *testing.T) { defer prepareTestEnv(t)() - issue1 := unittest.AssertExistsAndLoadBean(t, &models.Issue{ID: 1}).(*models.Issue) - issue2 := unittest.AssertExistsAndLoadBean(t, &models.Issue{ID: 2}).(*models.Issue) - issue3 := unittest.AssertExistsAndLoadBean(t, &models.Issue{ID: 3}).(*models.Issue) - issue4 := unittest.AssertExistsAndLoadBean(t, &models.Issue{ID: 4}).(*models.Issue) - issue5 := unittest.AssertExistsAndLoadBean(t, &models.Issue{ID: 8}).(*models.Issue) + issue1 := unittest.AssertExistsAndLoadBean(t, &issues_model.Issue{ID: 1}).(*issues_model.Issue) + issue2 := unittest.AssertExistsAndLoadBean(t, &issues_model.Issue{ID: 2}).(*issues_model.Issue) + issue3 := unittest.AssertExistsAndLoadBean(t, &issues_model.Issue{ID: 3}).(*issues_model.Issue) + issue4 := unittest.AssertExistsAndLoadBean(t, &issues_model.Issue{ID: 4}).(*issues_model.Issue) + issue5 := unittest.AssertExistsAndLoadBean(t, &issues_model.Issue{ID: 8}).(*issues_model.Issue) owner := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: issue1.PosterID}).(*user_model.User) session := loginUser(t, owner.Name) token := getTokenForLoggedInUser(t, session) - testSubscription := func(issue *models.Issue, isWatching bool) { + testSubscription := func(issue *issues_model.Issue, isWatching bool) { issueRepo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: issue.RepoID}).(*repo_model.Repository) urlStr := fmt.Sprintf("/api/v1/repos/%s/%s/issues/%d/subscriptions/check?token=%s", issueRepo.OwnerName, issueRepo.Name, issue.Index, token) diff --git a/integrations/api_issue_test.go b/integrations/api_issue_test.go index cc7d8d6bd5..5c802e8d20 100644 --- a/integrations/api_issue_test.go +++ b/integrations/api_issue_test.go @@ -11,7 +11,8 @@ import ( "testing" "time" - "code.gitea.io/gitea/models" + "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" @@ -34,9 +35,9 @@ func TestAPIListIssues(t *testing.T) { resp := session.MakeRequest(t, NewRequest(t, "GET", link.String()), http.StatusOK) var apiIssues []*api.Issue DecodeJSON(t, resp, &apiIssues) - assert.Len(t, apiIssues, unittest.GetCount(t, &models.Issue{RepoID: repo.ID})) + assert.Len(t, apiIssues, unittest.GetCount(t, &issues_model.Issue{RepoID: repo.ID})) for _, apiIssue := range apiIssues { - unittest.AssertExistsAndLoadBean(t, &models.Issue{ID: apiIssue.ID, RepoID: repo.ID}) + unittest.AssertExistsAndLoadBean(t, &issues_model.Issue{ID: apiIssue.ID, RepoID: repo.ID}) } // test milestone filter @@ -91,7 +92,7 @@ func TestAPICreateIssue(t *testing.T) { assert.Equal(t, body, apiIssue.Body) assert.Equal(t, title, apiIssue.Title) - unittest.AssertExistsAndLoadBean(t, &models.Issue{ + unittest.AssertExistsAndLoadBean(t, &issues_model.Issue{ RepoID: repoBefore.ID, AssigneeID: owner.ID, Content: body, @@ -106,10 +107,10 @@ func TestAPICreateIssue(t *testing.T) { func TestAPIEditIssue(t *testing.T) { defer prepareTestEnv(t)() - issueBefore := unittest.AssertExistsAndLoadBean(t, &models.Issue{ID: 10}).(*models.Issue) + issueBefore := unittest.AssertExistsAndLoadBean(t, &issues_model.Issue{ID: 10}).(*issues_model.Issue) repoBefore := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: issueBefore.RepoID}).(*repo_model.Repository) owner := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: repoBefore.OwnerID}).(*user_model.User) - assert.NoError(t, issueBefore.LoadAttributes()) + assert.NoError(t, issueBefore.LoadAttributes(db.DefaultContext)) assert.Equal(t, int64(1019307200), int64(issueBefore.DeadlineUnix)) assert.Equal(t, api.StateOpen, issueBefore.State()) @@ -137,12 +138,12 @@ func TestAPIEditIssue(t *testing.T) { var apiIssue api.Issue DecodeJSON(t, resp, &apiIssue) - issueAfter := unittest.AssertExistsAndLoadBean(t, &models.Issue{ID: 10}).(*models.Issue) + issueAfter := unittest.AssertExistsAndLoadBean(t, &issues_model.Issue{ID: 10}).(*issues_model.Issue) repoAfter := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: issueBefore.RepoID}).(*repo_model.Repository) // check deleted user assert.Equal(t, int64(500), issueAfter.PosterID) - assert.NoError(t, issueAfter.LoadAttributes()) + assert.NoError(t, issueAfter.LoadAttributes(db.DefaultContext)) assert.Equal(t, int64(-1), issueAfter.PosterID) assert.Equal(t, int64(-1), issueBefore.PosterID) assert.Equal(t, int64(-1), apiIssue.Poster.ID) diff --git a/integrations/api_issue_tracked_time_test.go b/integrations/api_issue_tracked_time_test.go index 7c69d4eb9e..a6846cb786 100644 --- a/integrations/api_issue_tracked_time_test.go +++ b/integrations/api_issue_tracked_time_test.go @@ -10,8 +10,8 @@ import ( "testing" "time" - "code.gitea.io/gitea/models" "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" api "code.gitea.io/gitea/modules/structs" @@ -23,7 +23,7 @@ func TestAPIGetTrackedTimes(t *testing.T) { defer prepareTestEnv(t)() user2 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2}).(*user_model.User) - issue2 := unittest.AssertExistsAndLoadBean(t, &models.Issue{ID: 2}).(*models.Issue) + issue2 := unittest.AssertExistsAndLoadBean(t, &issues_model.Issue{ID: 2}).(*issues_model.Issue) assert.NoError(t, issue2.LoadRepo(db.DefaultContext)) session := loginUser(t, user2.Name) @@ -33,7 +33,7 @@ func TestAPIGetTrackedTimes(t *testing.T) { resp := session.MakeRequest(t, req, http.StatusOK) var apiTimes api.TrackedTimeList DecodeJSON(t, resp, &apiTimes) - expect, err := models.GetTrackedTimes(db.DefaultContext, &models.FindTrackedTimesOptions{IssueID: issue2.ID}) + expect, err := issues_model.GetTrackedTimes(db.DefaultContext, &issues_model.FindTrackedTimesOptions{IssueID: issue2.ID}) assert.NoError(t, err) assert.Len(t, apiTimes, 3) @@ -64,8 +64,8 @@ func TestAPIGetTrackedTimes(t *testing.T) { func TestAPIDeleteTrackedTime(t *testing.T) { defer prepareTestEnv(t)() - time6 := unittest.AssertExistsAndLoadBean(t, &models.TrackedTime{ID: 6}).(*models.TrackedTime) - issue2 := unittest.AssertExistsAndLoadBean(t, &models.Issue{ID: 2}).(*models.Issue) + time6 := unittest.AssertExistsAndLoadBean(t, &issues_model.TrackedTime{ID: 6}).(*issues_model.TrackedTime) + issue2 := unittest.AssertExistsAndLoadBean(t, &issues_model.Issue{ID: 2}).(*issues_model.Issue) assert.NoError(t, issue2.LoadRepo(db.DefaultContext)) user2 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2}).(*user_model.User) @@ -76,14 +76,14 @@ func TestAPIDeleteTrackedTime(t *testing.T) { req := NewRequestf(t, "DELETE", "/api/v1/repos/%s/%s/issues/%d/times/%d?token=%s", user2.Name, issue2.Repo.Name, issue2.Index, time6.ID, token) session.MakeRequest(t, req, http.StatusForbidden) - time3 := unittest.AssertExistsAndLoadBean(t, &models.TrackedTime{ID: 3}).(*models.TrackedTime) + time3 := unittest.AssertExistsAndLoadBean(t, &issues_model.TrackedTime{ID: 3}).(*issues_model.TrackedTime) req = NewRequestf(t, "DELETE", "/api/v1/repos/%s/%s/issues/%d/times/%d?token=%s", user2.Name, issue2.Repo.Name, issue2.Index, time3.ID, token) session.MakeRequest(t, req, http.StatusNoContent) // Delete non existing time session.MakeRequest(t, req, http.StatusNotFound) // Reset time of user 2 on issue 2 - trackedSeconds, err := models.GetTrackedSeconds(db.DefaultContext, models.FindTrackedTimesOptions{IssueID: 2, UserID: 2}) + trackedSeconds, err := issues_model.GetTrackedSeconds(db.DefaultContext, issues_model.FindTrackedTimesOptions{IssueID: 2, UserID: 2}) assert.NoError(t, err) assert.Equal(t, int64(3661), trackedSeconds) @@ -91,7 +91,7 @@ func TestAPIDeleteTrackedTime(t *testing.T) { session.MakeRequest(t, req, http.StatusNoContent) session.MakeRequest(t, req, http.StatusNotFound) - trackedSeconds, err = models.GetTrackedSeconds(db.DefaultContext, models.FindTrackedTimesOptions{IssueID: 2, UserID: 2}) + trackedSeconds, err = issues_model.GetTrackedSeconds(db.DefaultContext, issues_model.FindTrackedTimesOptions{IssueID: 2, UserID: 2}) assert.NoError(t, err) assert.Equal(t, int64(0), trackedSeconds) } @@ -99,7 +99,7 @@ func TestAPIDeleteTrackedTime(t *testing.T) { func TestAPIAddTrackedTimes(t *testing.T) { defer prepareTestEnv(t)() - issue2 := unittest.AssertExistsAndLoadBean(t, &models.Issue{ID: 2}).(*models.Issue) + issue2 := unittest.AssertExistsAndLoadBean(t, &issues_model.Issue{ID: 2}).(*issues_model.Issue) assert.NoError(t, issue2.LoadRepo(db.DefaultContext)) user2 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2}).(*user_model.User) admin := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 1}).(*user_model.User) diff --git a/integrations/api_pull_commits_test.go b/integrations/api_pull_commits_test.go index 5e057b05a1..3b75fbcb4a 100644 --- a/integrations/api_pull_commits_test.go +++ b/integrations/api_pull_commits_test.go @@ -8,7 +8,7 @@ import ( "net/http" "testing" - "code.gitea.io/gitea/models" + issues_model "code.gitea.io/gitea/models/issues" repo_model "code.gitea.io/gitea/models/repo" "code.gitea.io/gitea/models/unittest" api "code.gitea.io/gitea/modules/structs" @@ -18,7 +18,7 @@ import ( func TestAPIPullCommits(t *testing.T) { defer prepareTestEnv(t)() - pullIssue := unittest.AssertExistsAndLoadBean(t, &models.PullRequest{ID: 2}).(*models.PullRequest) + pullIssue := unittest.AssertExistsAndLoadBean(t, &issues_model.PullRequest{ID: 2}).(*issues_model.PullRequest) assert.NoError(t, pullIssue.LoadIssue()) repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: pullIssue.HeadRepoID}).(*repo_model.Repository) diff --git a/integrations/api_pull_review_test.go b/integrations/api_pull_review_test.go index 3f80dbdf9b..b601ca1d41 100644 --- a/integrations/api_pull_review_test.go +++ b/integrations/api_pull_review_test.go @@ -9,7 +9,8 @@ import ( "net/http" "testing" - "code.gitea.io/gitea/models" + "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/json" @@ -20,8 +21,8 @@ import ( func TestAPIPullReview(t *testing.T) { defer prepareTestEnv(t)() - pullIssue := unittest.AssertExistsAndLoadBean(t, &models.Issue{ID: 3}).(*models.Issue) - assert.NoError(t, pullIssue.LoadAttributes()) + pullIssue := unittest.AssertExistsAndLoadBean(t, &issues_model.Issue{ID: 3}).(*issues_model.Issue) + assert.NoError(t, pullIssue.LoadAttributes(db.DefaultContext)) repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: pullIssue.RepoID}).(*repo_model.Repository) // test ListPullReviews @@ -64,7 +65,7 @@ func TestAPIPullReview(t *testing.T) { assert.EqualValues(t, *reviews[5], review) // test GetPullReviewComments - comment := unittest.AssertExistsAndLoadBean(t, &models.Comment{ID: 7}).(*models.Comment) + comment := unittest.AssertExistsAndLoadBean(t, &issues_model.Comment{ID: 7}).(*issues_model.Comment) req = NewRequestf(t, http.MethodGet, "/api/v1/repos/%s/%s/pulls/%d/reviews/%d/comments?token=%s", repo.OwnerName, repo.Name, pullIssue.Index, 10, token) resp = session.MakeRequest(t, req, http.StatusOK) var reviewComments []*api.PullReviewComment @@ -199,8 +200,8 @@ func TestAPIPullReview(t *testing.T) { // test get review requests // to make it simple, use same api with get review - pullIssue12 := unittest.AssertExistsAndLoadBean(t, &models.Issue{ID: 12}).(*models.Issue) - assert.NoError(t, pullIssue12.LoadAttributes()) + pullIssue12 := unittest.AssertExistsAndLoadBean(t, &issues_model.Issue{ID: 12}).(*issues_model.Issue) + assert.NoError(t, pullIssue12.LoadAttributes(db.DefaultContext)) repo3 := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: pullIssue12.RepoID}).(*repo_model.Repository) req = NewRequestf(t, http.MethodGet, "/api/v1/repos/%s/%s/pulls/%d/reviews?token=%s", repo3.OwnerName, repo3.Name, pullIssue12.Index, token) @@ -223,8 +224,8 @@ func TestAPIPullReview(t *testing.T) { func TestAPIPullReviewRequest(t *testing.T) { defer prepareTestEnv(t)() - pullIssue := unittest.AssertExistsAndLoadBean(t, &models.Issue{ID: 3}).(*models.Issue) - assert.NoError(t, pullIssue.LoadAttributes()) + pullIssue := unittest.AssertExistsAndLoadBean(t, &issues_model.Issue{ID: 3}).(*issues_model.Issue) + assert.NoError(t, pullIssue.LoadAttributes(db.DefaultContext)) repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: pullIssue.RepoID}).(*repo_model.Repository) // Test add Review Request @@ -268,8 +269,8 @@ func TestAPIPullReviewRequest(t *testing.T) { session.MakeRequest(t, req, http.StatusNoContent) // Test team review request - pullIssue12 := unittest.AssertExistsAndLoadBean(t, &models.Issue{ID: 12}).(*models.Issue) - assert.NoError(t, pullIssue12.LoadAttributes()) + pullIssue12 := unittest.AssertExistsAndLoadBean(t, &issues_model.Issue{ID: 12}).(*issues_model.Issue) + assert.NoError(t, pullIssue12.LoadAttributes(db.DefaultContext)) repo3 := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: pullIssue12.RepoID}).(*repo_model.Repository) // Test add Team Review Request diff --git a/integrations/api_pull_test.go b/integrations/api_pull_test.go index a1c2a4c3e6..0c63ec2c00 100644 --- a/integrations/api_pull_test.go +++ b/integrations/api_pull_test.go @@ -9,7 +9,7 @@ import ( "net/http" "testing" - "code.gitea.io/gitea/models" + 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" @@ -33,7 +33,7 @@ func TestAPIViewPulls(t *testing.T) { var pulls []*api.PullRequest DecodeJSON(t, resp, &pulls) - expectedLen := unittest.GetCount(t, &models.Issue{RepoID: repo.ID}, unittest.Cond("is_pull = ?", true)) + expectedLen := unittest.GetCount(t, &issues_model.Issue{RepoID: repo.ID}, unittest.Cond("is_pull = ?", true)) assert.Len(t, pulls, expectedLen) } @@ -42,7 +42,7 @@ func TestAPIMergePullWIP(t *testing.T) { defer prepareTestEnv(t)() repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 1}).(*repo_model.Repository) owner := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: repo.OwnerID}).(*user_model.User) - pr := unittest.AssertExistsAndLoadBean(t, &models.PullRequest{Status: models.PullRequestStatusMergeable}, unittest.Cond("has_merged = ?", false)).(*models.PullRequest) + pr := unittest.AssertExistsAndLoadBean(t, &issues_model.PullRequest{Status: issues_model.PullRequestStatusMergeable}, unittest.Cond("has_merged = ?", false)).(*issues_model.PullRequest) pr.LoadIssue() issue_service.ChangeTitle(pr.Issue, owner, setting.Repository.PullRequest.WorkInProgressPrefixes[0]+" "+pr.Issue.Title) diff --git a/integrations/delete_user_test.go b/integrations/delete_user_test.go index cf376f6fcc..8b86780224 100644 --- a/integrations/delete_user_test.go +++ b/integrations/delete_user_test.go @@ -9,7 +9,7 @@ import ( "net/http" "testing" - "code.gitea.io/gitea/models" + 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" @@ -24,7 +24,7 @@ func assertUserDeleted(t *testing.T, userID int64) { unittest.AssertNotExistsBean(t, &repo_model.Repository{OwnerID: userID}) unittest.AssertNotExistsBean(t, &access_model.Access{UserID: userID}) unittest.AssertNotExistsBean(t, &organization.OrgUser{UID: userID}) - unittest.AssertNotExistsBean(t, &models.IssueUser{UID: userID}) + unittest.AssertNotExistsBean(t, &issues_model.IssueUser{UID: userID}) unittest.AssertNotExistsBean(t, &organization.TeamUser{UID: userID}) unittest.AssertNotExistsBean(t, &repo_model.Star{UID: userID}) } diff --git a/integrations/git_test.go b/integrations/git_test.go index 63afc7913b..a3ba7b7aa9 100644 --- a/integrations/git_test.go +++ b/integrations/git_test.go @@ -17,8 +17,8 @@ import ( "testing" "time" - "code.gitea.io/gitea/models" "code.gitea.io/gitea/models/db" + issues_model "code.gitea.io/gitea/models/issues" "code.gitea.io/gitea/models/perm" repo_model "code.gitea.io/gitea/models/repo" "code.gitea.io/gitea/models/unittest" @@ -715,7 +715,7 @@ func doCreateAgitFlowPull(dstPath string, ctx *APITestContext, baseBranch, headB defer gitRepo.Close() var ( - pr1, pr2 *models.PullRequest + pr1, pr2 *issues_model.PullRequest commit string ) repo, err := repo_model.GetRepositoryByOwnerAndName(ctx.Username, ctx.Reponame) @@ -723,7 +723,7 @@ func doCreateAgitFlowPull(dstPath string, ctx *APITestContext, baseBranch, headB return } - pullNum := unittest.GetCount(t, &models.PullRequest{}) + pullNum := unittest.GetCount(t, &issues_model.PullRequest{}) t.Run("CreateHeadBranch", doGitCreateBranch(dstPath, headBranch)) @@ -759,11 +759,11 @@ func doCreateAgitFlowPull(dstPath string, ctx *APITestContext, baseBranch, headB if !assert.NoError(t, err) { return } - unittest.AssertCount(t, &models.PullRequest{}, pullNum+1) - pr1 = unittest.AssertExistsAndLoadBean(t, &models.PullRequest{ + unittest.AssertCount(t, &issues_model.PullRequest{}, pullNum+1) + pr1 = unittest.AssertExistsAndLoadBean(t, &issues_model.PullRequest{ HeadRepoID: repo.ID, - Flow: models.PullRequestFlowAGit, - }).(*models.PullRequest) + Flow: issues_model.PullRequestFlowAGit, + }).(*issues_model.PullRequest) if !assert.NotEmpty(t, pr1) { return } @@ -780,12 +780,12 @@ func doCreateAgitFlowPull(dstPath string, ctx *APITestContext, baseBranch, headB if !assert.NoError(t, err) { return } - unittest.AssertCount(t, &models.PullRequest{}, pullNum+2) - pr2 = unittest.AssertExistsAndLoadBean(t, &models.PullRequest{ + unittest.AssertCount(t, &issues_model.PullRequest{}, pullNum+2) + pr2 = unittest.AssertExistsAndLoadBean(t, &issues_model.PullRequest{ HeadRepoID: repo.ID, Index: pr1.Index + 1, - Flow: models.PullRequestFlowAGit, - }).(*models.PullRequest) + Flow: issues_model.PullRequestFlowAGit, + }).(*issues_model.PullRequest) if !assert.NotEmpty(t, pr2) { return } @@ -833,7 +833,7 @@ func doCreateAgitFlowPull(dstPath string, ctx *APITestContext, baseBranch, headB if !assert.NoError(t, err) { return } - unittest.AssertCount(t, &models.PullRequest{}, pullNum+2) + unittest.AssertCount(t, &issues_model.PullRequest{}, pullNum+2) prMsg, err := doAPIGetPullRequest(*ctx, ctx.Username, ctx.Reponame, pr1.Index)(t) if !assert.NoError(t, err) { return @@ -845,7 +845,7 @@ func doCreateAgitFlowPull(dstPath string, ctx *APITestContext, baseBranch, headB if !assert.NoError(t, err) { return } - unittest.AssertCount(t, &models.PullRequest{}, pullNum+2) + unittest.AssertCount(t, &issues_model.PullRequest{}, pullNum+2) prMsg, err = doAPIGetPullRequest(*ctx, ctx.Username, ctx.Reponame, pr2.Index)(t) if !assert.NoError(t, err) { return diff --git a/integrations/issue_test.go b/integrations/issue_test.go index 8e04b99d5e..7d30d657f5 100644 --- a/integrations/issue_test.go +++ b/integrations/issue_test.go @@ -14,7 +14,8 @@ import ( "testing" "time" - "code.gitea.io/gitea/models" + "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" @@ -34,16 +35,16 @@ func getIssuesSelection(t testing.TB, htmlDoc *HTMLDoc) *goquery.Selection { return issueList.Find("li").Find(".title") } -func getIssue(t *testing.T, repoID int64, issueSelection *goquery.Selection) *models.Issue { +func getIssue(t *testing.T, repoID int64, issueSelection *goquery.Selection) *issues_model.Issue { href, exists := issueSelection.Attr("href") assert.True(t, exists) indexStr := href[strings.LastIndexByte(href, '/')+1:] index, err := strconv.Atoi(indexStr) assert.NoError(t, err, "Invalid issue href: %s", href) - return unittest.AssertExistsAndLoadBean(t, &models.Issue{RepoID: repoID, Index: int64(index)}).(*models.Issue) + return unittest.AssertExistsAndLoadBean(t, &issues_model.Issue{RepoID: repoID, Index: int64(index)}).(*issues_model.Issue) } -func assertMatch(t testing.TB, issue *models.Issue, keyword string) { +func assertMatch(t testing.TB, issue *issues_model.Issue, keyword string) { matches := strings.Contains(strings.ToLower(issue.Title), keyword) || strings.Contains(strings.ToLower(issue.Content), keyword) for _, comment := range issue.Comments { @@ -75,7 +76,7 @@ func TestViewIssuesSortByType(t *testing.T) { htmlDoc := NewHTMLParser(t, resp.Body) issuesSelection := getIssuesSelection(t, htmlDoc) expectedNumIssues := unittest.GetCount(t, - &models.Issue{RepoID: repo.ID, PosterID: user.ID}, + &issues_model.Issue{RepoID: repo.ID, PosterID: user.ID}, unittest.Cond("is_closed=?", false), unittest.Cond("is_pull=?", false), ) @@ -94,10 +95,10 @@ func TestViewIssuesKeyword(t *testing.T) { defer prepareTestEnv(t)() repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 1}).(*repo_model.Repository) - issue := unittest.AssertExistsAndLoadBean(t, &models.Issue{ + issue := unittest.AssertExistsAndLoadBean(t, &issues_model.Issue{ RepoID: repo.ID, Index: 1, - }).(*models.Issue) + }).(*issues_model.Issue) issues.UpdateIssueIndexer(issue) time.Sleep(time.Second * 1) const keyword = "first" @@ -238,7 +239,7 @@ func TestIssueCrossReference(t *testing.T) { // Ref from issue title issueRefURL, issueRef := testIssueWithBean(t, "user2", 1, fmt.Sprintf("Title ref #%d", issueBase.Index), "Description") - unittest.AssertExistsAndLoadBean(t, &models.Comment{ + unittest.AssertExistsAndLoadBean(t, &issues_model.Comment{ IssueID: issueBase.ID, RefRepoID: 1, RefIssueID: issueRef.ID, @@ -249,7 +250,7 @@ func TestIssueCrossReference(t *testing.T) { // Edit title, neuter ref testIssueChangeInfo(t, "user2", issueRefURL, "title", "Title no ref") - unittest.AssertExistsAndLoadBean(t, &models.Comment{ + unittest.AssertExistsAndLoadBean(t, &issues_model.Comment{ IssueID: issueBase.ID, RefRepoID: 1, RefIssueID: issueRef.ID, @@ -260,7 +261,7 @@ func TestIssueCrossReference(t *testing.T) { // Ref from issue content issueRefURL, issueRef = testIssueWithBean(t, "user2", 1, "TitleXRef", fmt.Sprintf("Description ref #%d", issueBase.Index)) - unittest.AssertExistsAndLoadBean(t, &models.Comment{ + unittest.AssertExistsAndLoadBean(t, &issues_model.Comment{ IssueID: issueBase.ID, RefRepoID: 1, RefIssueID: issueRef.ID, @@ -271,7 +272,7 @@ func TestIssueCrossReference(t *testing.T) { // Edit content, neuter ref testIssueChangeInfo(t, "user2", issueRefURL, "content", "Description no ref") - unittest.AssertExistsAndLoadBean(t, &models.Comment{ + unittest.AssertExistsAndLoadBean(t, &issues_model.Comment{ IssueID: issueBase.ID, RefRepoID: 1, RefIssueID: issueRef.ID, @@ -283,7 +284,7 @@ func TestIssueCrossReference(t *testing.T) { // Ref from a comment session := loginUser(t, "user2") commentID := testIssueAddComment(t, session, issueRefURL, fmt.Sprintf("Adding ref from comment #%d", issueBase.Index), "") - comment := &models.Comment{ + comment := &issues_model.Comment{ IssueID: issueBase.ID, RefRepoID: 1, RefIssueID: issueRef.ID, @@ -295,7 +296,7 @@ func TestIssueCrossReference(t *testing.T) { // Ref from a different repository _, issueRef = testIssueWithBean(t, "user12", 10, "TitleXRef", fmt.Sprintf("Description ref user2/repo1#%d", issueBase.Index)) - unittest.AssertExistsAndLoadBean(t, &models.Comment{ + unittest.AssertExistsAndLoadBean(t, &issues_model.Comment{ IssueID: issueBase.ID, RefRepoID: 10, RefIssueID: issueRef.ID, @@ -305,13 +306,13 @@ func TestIssueCrossReference(t *testing.T) { }) } -func testIssueWithBean(t *testing.T, user string, repoID int64, title, content string) (string, *models.Issue) { +func testIssueWithBean(t *testing.T, user string, repoID int64, title, content string) (string, *issues_model.Issue) { session := loginUser(t, user) issueURL := testNewIssue(t, session, user, fmt.Sprintf("repo%d", repoID), title, content) indexStr := issueURL[strings.LastIndexByte(issueURL, '/')+1:] index, err := strconv.Atoi(indexStr) assert.NoError(t, err, "Invalid issue href: %s", issueURL) - issue := &models.Issue{RepoID: repoID, Index: int64(index)} + issue := &issues_model.Issue{RepoID: repoID, Index: int64(index)} unittest.AssertExistsAndLoadBean(t, issue) return issueURL, issue } @@ -511,10 +512,10 @@ func TestSearchIssuesWithLabels(t *testing.T) { func TestGetIssueInfo(t *testing.T) { defer prepareTestEnv(t)() - issue := unittest.AssertExistsAndLoadBean(t, &models.Issue{ID: 10}).(*models.Issue) + issue := unittest.AssertExistsAndLoadBean(t, &issues_model.Issue{ID: 10}).(*issues_model.Issue) repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: issue.RepoID}).(*repo_model.Repository) owner := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: repo.OwnerID}).(*user_model.User) - assert.NoError(t, issue.LoadAttributes()) + assert.NoError(t, issue.LoadAttributes(db.DefaultContext)) assert.Equal(t, int64(1019307200), int64(issue.DeadlineUnix)) assert.Equal(t, api.StateOpen, issue.State()) @@ -532,10 +533,10 @@ func TestGetIssueInfo(t *testing.T) { func TestUpdateIssueDeadline(t *testing.T) { defer prepareTestEnv(t)() - issueBefore := unittest.AssertExistsAndLoadBean(t, &models.Issue{ID: 10}).(*models.Issue) + issueBefore := unittest.AssertExistsAndLoadBean(t, &issues_model.Issue{ID: 10}).(*issues_model.Issue) repoBefore := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: issueBefore.RepoID}).(*repo_model.Repository) owner := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: repoBefore.OwnerID}).(*user_model.User) - assert.NoError(t, issueBefore.LoadAttributes()) + assert.NoError(t, issueBefore.LoadAttributes(db.DefaultContext)) assert.Equal(t, int64(1019307200), int64(issueBefore.DeadlineUnix)) assert.Equal(t, api.StateOpen, issueBefore.State()) diff --git a/integrations/pull_merge_test.go b/integrations/pull_merge_test.go index 6c5b67caa6..de519094d4 100644 --- a/integrations/pull_merge_test.go +++ b/integrations/pull_merge_test.go @@ -19,6 +19,7 @@ import ( "code.gitea.io/gitea/models" "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" @@ -233,12 +234,12 @@ func TestCantMergeConflict(t *testing.T) { Name: "repo1", }).(*repo_model.Repository) - pr := unittest.AssertExistsAndLoadBean(t, &models.PullRequest{ + pr := unittest.AssertExistsAndLoadBean(t, &issues_model.PullRequest{ HeadRepoID: repo1.ID, BaseRepoID: repo1.ID, HeadBranch: "conflict", BaseBranch: "base", - }).(*models.PullRequest) + }).(*issues_model.PullRequest) gitRepo, err := git.OpenRepository(git.DefaultContext, repo_model.RepoPath(user1.Name, repo1.Name)) assert.NoError(t, err) @@ -335,12 +336,12 @@ func TestCantMergeUnrelated(t *testing.T) { // Now this PR could be marked conflict - or at least a race may occur - so drop down to pure code at this point... gitRepo, err := git.OpenRepository(git.DefaultContext, path) assert.NoError(t, err) - pr := unittest.AssertExistsAndLoadBean(t, &models.PullRequest{ + pr := unittest.AssertExistsAndLoadBean(t, &issues_model.PullRequest{ HeadRepoID: repo1.ID, BaseRepoID: repo1.ID, HeadBranch: "unrelated", BaseBranch: "base", - }).(*models.PullRequest) + }).(*issues_model.PullRequest) err = pull.Merge(context.Background(), pr, user1, gitRepo, repo_model.MergeStyleMerge, "", "UNRELATED") assert.Error(t, err, "Merge should return an error due to unrelated") @@ -387,7 +388,7 @@ func TestConflictChecking(t *testing.T) { assert.NoError(t, err) // create Pull to merge the important-secrets branch into main branch. - pullIssue := &models.Issue{ + pullIssue := &issues_model.Issue{ RepoID: baseRepo.ID, Title: "PR with conflict!", PosterID: user.ID, @@ -395,26 +396,26 @@ func TestConflictChecking(t *testing.T) { IsPull: true, } - pullRequest := &models.PullRequest{ + pullRequest := &issues_model.PullRequest{ HeadRepoID: baseRepo.ID, BaseRepoID: baseRepo.ID, HeadBranch: "important-secrets", BaseBranch: "main", HeadRepo: baseRepo, BaseRepo: baseRepo, - Type: models.PullRequestGitea, + Type: issues_model.PullRequestGitea, } err = pull.NewPullRequest(git.DefaultContext, baseRepo, pullIssue, nil, nil, pullRequest, nil) assert.NoError(t, err) - issue := unittest.AssertExistsAndLoadBean(t, &models.Issue{Title: "PR with conflict!"}).(*models.Issue) - conflictingPR, err := models.GetPullRequestByIssueID(db.DefaultContext, issue.ID) + issue := unittest.AssertExistsAndLoadBean(t, &issues_model.Issue{Title: "PR with conflict!"}).(*issues_model.Issue) + conflictingPR, err := issues_model.GetPullRequestByIssueID(db.DefaultContext, issue.ID) assert.NoError(t, err) // Ensure conflictedFiles is populated. assert.Equal(t, 1, len(conflictingPR.ConflictedFiles)) // Check if status is correct. - assert.Equal(t, models.PullRequestStatusConflict, conflictingPR.Status) + assert.Equal(t, issues_model.PullRequestStatusConflict, conflictingPR.Status) // Ensure that mergeable returns false assert.False(t, conflictingPR.Mergeable()) }) diff --git a/integrations/pull_update_test.go b/integrations/pull_update_test.go index f11eacf144..47ada91e1a 100644 --- a/integrations/pull_update_test.go +++ b/integrations/pull_update_test.go @@ -12,6 +12,7 @@ import ( "code.gitea.io/gitea/models" "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/git" @@ -78,7 +79,7 @@ func TestAPIPullUpdateByRebase(t *testing.T) { }) } -func createOutdatedPR(t *testing.T, actor, forkOrg *user_model.User) *models.PullRequest { +func createOutdatedPR(t *testing.T, actor, forkOrg *user_model.User) *issues_model.PullRequest { baseRepo, err := repo_service.CreateRepository(actor, actor, models.CreateRepoOptions{ Name: "repo-pr-update", Description: "repo-tmp-pr-update description", @@ -146,27 +147,27 @@ func createOutdatedPR(t *testing.T, actor, forkOrg *user_model.User) *models.Pul assert.NoError(t, err) // create Pull - pullIssue := &models.Issue{ + pullIssue := &issues_model.Issue{ RepoID: baseRepo.ID, Title: "Test Pull -to-update-", PosterID: actor.ID, Poster: actor, IsPull: true, } - pullRequest := &models.PullRequest{ + pullRequest := &issues_model.PullRequest{ HeadRepoID: headRepo.ID, BaseRepoID: baseRepo.ID, HeadBranch: "newBranch", BaseBranch: "master", HeadRepo: headRepo, BaseRepo: baseRepo, - Type: models.PullRequestGitea, + Type: issues_model.PullRequestGitea, } err = pull_service.NewPullRequest(git.DefaultContext, baseRepo, pullIssue, nil, nil, pullRequest, nil) assert.NoError(t, err) - issue := unittest.AssertExistsAndLoadBean(t, &models.Issue{Title: "Test Pull -to-update-"}).(*models.Issue) - pr, err := models.GetPullRequestByIssueID(db.DefaultContext, issue.ID) + issue := unittest.AssertExistsAndLoadBean(t, &issues_model.Issue{Title: "Test Pull -to-update-"}).(*issues_model.Issue) + pr, err := issues_model.GetPullRequestByIssueID(db.DefaultContext, issue.ID) assert.NoError(t, err) return pr diff --git a/integrations/user_test.go b/integrations/user_test.go index d0523d8b3a..6a3d30472d 100644 --- a/integrations/user_test.go +++ b/integrations/user_test.go @@ -8,7 +8,7 @@ import ( "net/http" "testing" - "code.gitea.io/gitea/models" + 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" @@ -237,8 +237,8 @@ func TestListStopWatches(t *testing.T) { resp := session.MakeRequest(t, req, http.StatusOK) var apiWatches []*api.StopWatch DecodeJSON(t, resp, &apiWatches) - stopwatch := unittest.AssertExistsAndLoadBean(t, &models.Stopwatch{UserID: owner.ID}).(*models.Stopwatch) - issue := unittest.AssertExistsAndLoadBean(t, &models.Issue{ID: stopwatch.IssueID}).(*models.Issue) + stopwatch := unittest.AssertExistsAndLoadBean(t, &issues_model.Stopwatch{UserID: owner.ID}).(*issues_model.Stopwatch) + issue := unittest.AssertExistsAndLoadBean(t, &issues_model.Issue{ID: stopwatch.IssueID}).(*issues_model.Issue) if assert.Len(t, apiWatches, 1) { assert.EqualValues(t, stopwatch.CreatedUnix.AsTime().Unix(), apiWatches[0].Created.Unix()) assert.EqualValues(t, issue.Index, apiWatches[0].IssueIndex) 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.go b/models/issue.go deleted file mode 100644 index a22c115523..0000000000 --- a/models/issue.go +++ /dev/null @@ -1,2479 +0,0 @@ -// Copyright 2014 The Gogs Authors. All rights reserved. -// 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 ( - "context" - "fmt" - "regexp" - "sort" - "strconv" - "strings" - - 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" - project_model "code.gitea.io/gitea/models/project" - repo_model "code.gitea.io/gitea/models/repo" - "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/git" - "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" - - "xorm.io/builder" - "xorm.io/xorm" -) - -// Issue represents an issue or pull request of repository. -type Issue struct { - ID int64 `xorm:"pk autoincr"` - RepoID int64 `xorm:"INDEX UNIQUE(repo_index)"` - Repo *repo_model.Repository `xorm:"-"` - Index int64 `xorm:"UNIQUE(repo_index)"` // Index in one repository. - 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:"-"` - Priority int - AssigneeID int64 `xorm:"-"` - Assignee *user_model.User `xorm:"-"` - IsClosed bool `xorm:"INDEX"` - IsRead bool `xorm:"-"` - IsPull bool `xorm:"INDEX"` // Indicates whether is a pull request or not. - PullRequest *PullRequest `xorm:"-"` - NumComments int - Ref string - - DeadlineUnix timeutil.TimeStamp `xorm:"INDEX"` - - CreatedUnix timeutil.TimeStamp `xorm:"INDEX created"` - UpdatedUnix timeutil.TimeStamp `xorm:"INDEX updated"` - ClosedUnix timeutil.TimeStamp `xorm:"INDEX"` - - Attachments []*repo_model.Attachment `xorm:"-"` - Comments []*Comment `xorm:"-"` - Reactions issues_model.ReactionList `xorm:"-"` - TotalTrackedTime int64 `xorm:"-"` - Assignees []*user_model.User `xorm:"-"` - ForeignReference *foreignreference.ForeignReference `xorm:"-"` - - // IsLocked limits commenting abilities to users on an issue - // with write access - IsLocked bool `xorm:"NOT NULL DEFAULT false"` - - // For view issue page. - ShowRole RoleDescriptor `xorm:"-"` -} - -var ( - issueTasksPat *regexp.Regexp - issueTasksDonePat *regexp.Regexp -) - -const ( - issueTasksRegexpStr = `(^\s*[-*]\s\[[\sxX]\]\s.)|(\n\s*[-*]\s\[[\sxX]\]\s.)` - issueTasksDoneRegexpStr = `(^\s*[-*]\s\[[xX]\]\s.)|(\n\s*[-*]\s\[[xX]\]\s.)` -) - -// IssueIndex represents the issue index table -type IssueIndex db.ResourceIndex - -func init() { - issueTasksPat = regexp.MustCompile(issueTasksRegexpStr) - issueTasksDonePat = regexp.MustCompile(issueTasksDoneRegexpStr) - - db.RegisterModel(new(Issue)) - db.RegisterModel(new(IssueIndex)) -} - -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 { - return err - } - return nil -} - -// IsOverdue checks if the issue is overdue -func (issue *Issue) IsOverdue() bool { - if issue.IsClosed { - return issue.ClosedUnix >= issue.DeadlineUnix - } - return timeutil.TimeStampNow() >= issue.DeadlineUnix -} - -// LoadRepo loads issue's repository -func (issue *Issue) LoadRepo(ctx context.Context) (err error) { - if issue.Repo == nil { - issue.Repo, err = repo_model.GetRepositoryByIDCtx(ctx, issue.RepoID) - if err != nil { - return fmt.Errorf("getRepositoryByID [%d]: %v", issue.RepoID, err) - } - } - return nil -} - -// IsTimetrackerEnabled returns true if the repo enables timetracking -func (issue *Issue) IsTimetrackerEnabled() bool { - return issue.isTimetrackerEnabled(db.DefaultContext) -} - -func (issue *Issue) isTimetrackerEnabled(ctx context.Context) bool { - if err := issue.LoadRepo(ctx); err != nil { - log.Error(fmt.Sprintf("loadRepo: %v", err)) - return false - } - return issue.Repo.IsTimetrackerEnabledCtx(ctx) -} - -// GetPullRequest returns the issue pull request -func (issue *Issue) GetPullRequest() (pr *PullRequest, err error) { - if !issue.IsPull { - return nil, fmt.Errorf("Issue is not a pull request") - } - - pr, err = GetPullRequestByIssueID(db.DefaultContext, issue.ID) - if err != nil { - return nil, err - } - pr.Issue = issue - return -} - -// LoadLabels loads labels -func (issue *Issue) LoadLabels(ctx context.Context) (err error) { - if issue.Labels == nil { - issue.Labels, err = GetLabelsByIssueID(ctx, issue.ID) - if err != nil { - return fmt.Errorf("getLabelsByIssueID [%d]: %v", issue.ID, err) - } - } - return nil -} - -// LoadPoster loads poster -func (issue *Issue) LoadPoster() error { - return issue.loadPoster(db.DefaultContext) -} - -func (issue *Issue) loadPoster(ctx context.Context) (err error) { - if issue.Poster == nil { - issue.Poster, err = user_model.GetUserByIDCtx(ctx, issue.PosterID) - if err != nil { - issue.PosterID = -1 - issue.Poster = user_model.NewGhostUser() - if !user_model.IsErrUserNotExist(err) { - return fmt.Errorf("getUserByID.(poster) [%d]: %v", issue.PosterID, err) - } - err = nil - return - } - } - return -} - -func (issue *Issue) loadPullRequest(ctx context.Context) (err error) { - if issue.IsPull && issue.PullRequest == nil { - issue.PullRequest, err = GetPullRequestByIssueID(ctx, issue.ID) - if err != nil { - if IsErrPullRequestNotExist(err) { - return err - } - return fmt.Errorf("getPullRequestByIssueID [%d]: %v", issue.ID, err) - } - issue.PullRequest.Issue = issue - } - return nil -} - -// LoadPullRequest loads pull request info -func (issue *Issue) LoadPullRequest() error { - return issue.loadPullRequest(db.DefaultContext) -} - -func (issue *Issue) loadComments(ctx context.Context) (err error) { - return issue.loadCommentsByType(ctx, CommentTypeUnknown) -} - -// LoadDiscussComments loads discuss comments -func (issue *Issue) LoadDiscussComments() error { - return issue.loadCommentsByType(db.DefaultContext, CommentTypeComment) -} - -func (issue *Issue) loadCommentsByType(ctx context.Context, tp CommentType) (err error) { - if issue.Comments != nil { - return nil - } - issue.Comments, err = FindComments(ctx, &FindCommentsOptions{ - IssueID: issue.ID, - Type: tp, - }) - return err -} - -func (issue *Issue) loadReactions(ctx context.Context) (err error) { - if issue.Reactions != nil { - return nil - } - reactions, _, err := issues_model.FindReactions(ctx, issues_model.FindReactionsOptions{ - IssueID: issue.ID, - }) - if err != nil { - return err - } - if err = issue.LoadRepo(ctx); err != nil { - return err - } - // Load reaction user data - if _, err := issues_model.ReactionList(reactions).LoadUsers(ctx, issue.Repo); err != nil { - return err - } - - // Cache comments to map - comments := make(map[int64]*Comment) - for _, comment := range issue.Comments { - comments[comment.ID] = comment - } - // Add reactions either to issue or comment - for _, react := range reactions { - if react.CommentID == 0 { - issue.Reactions = append(issue.Reactions, react) - } else if comment, ok := comments[react.CommentID]; ok { - comment.Reactions = append(comment.Reactions, react) - } - } - return nil -} - -func (issue *Issue) loadForeignReference(ctx context.Context) (err error) { - if issue.ForeignReference != nil { - return nil - } - reference := &foreignreference.ForeignReference{ - RepoID: issue.RepoID, - LocalIndex: issue.Index, - Type: foreignreference.TypeIssue, - } - has, err := db.GetEngine(ctx).Get(reference) - if err != nil { - return err - } else if !has { - return foreignreference.ErrForeignIndexNotExist{ - RepoID: issue.RepoID, - LocalIndex: issue.Index, - Type: foreignreference.TypeIssue, - } - } - issue.ForeignReference = reference - return nil -} - -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) { - 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) { - if err = issue.LoadRepo(ctx); err != nil { - return - } - - if err = issue.loadPoster(ctx); err != nil { - return - } - - if err = issue.LoadLabels(ctx); err != nil { - return - } - - if err = issue.loadMilestone(ctx); err != nil { - return - } - - if err = issue.loadProject(ctx); err != nil { - return - } - - if err = issue.LoadAssignees(ctx); err != nil { - return - } - - if err = issue.loadPullRequest(ctx); err != nil && !IsErrPullRequestNotExist(err) { - // It is possible pull request is not yet created. - return err - } - - if issue.Attachments == nil { - issue.Attachments, err = repo_model.GetAttachmentsByIssueID(ctx, issue.ID) - if err != nil { - return fmt.Errorf("getAttachmentsByIssueID [%d]: %v", issue.ID, err) - } - } - - if err = issue.loadComments(ctx); err != nil { - return err - } - - if err = CommentList(issue.Comments).loadAttributes(ctx); err != nil { - return err - } - if issue.isTimetrackerEnabled(ctx) { - if err = issue.loadTotalTimes(ctx); err != nil { - return err - } - } - - if err = issue.loadForeignReference(ctx); err != nil && !foreignreference.IsErrForeignIndexNotExist(err) { - return err - } - - 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) -} - -// GetIsRead load the `IsRead` field of the issue -func (issue *Issue) GetIsRead(userID int64) error { - issueUser := &IssueUser{IssueID: issue.ID, UID: userID} - if has, err := db.GetEngine(db.DefaultContext).Get(issueUser); err != nil { - return err - } else if !has { - issue.IsRead = false - return nil - } - issue.IsRead = issueUser.IsRead - return nil -} - -// APIURL returns the absolute APIURL to this issue. -func (issue *Issue) APIURL() string { - if issue.Repo == nil { - err := issue.LoadRepo(db.DefaultContext) - if err != nil { - log.Error("Issue[%d].APIURL(): %v", issue.ID, err) - return "" - } - } - return fmt.Sprintf("%s/issues/%d", issue.Repo.APIURL(), issue.Index) -} - -// HTMLURL returns the absolute URL to this issue. -func (issue *Issue) HTMLURL() string { - var path string - if issue.IsPull { - path = "pulls" - } else { - path = "issues" - } - return fmt.Sprintf("%s/%s/%d", issue.Repo.HTMLURL(), path, issue.Index) -} - -// Link returns the Link URL to this issue. -func (issue *Issue) Link() string { - var path string - if issue.IsPull { - path = "pulls" - } else { - path = "issues" - } - return fmt.Sprintf("%s/%s/%d", issue.Repo.Link(), path, issue.Index) -} - -// DiffURL returns the absolute URL to this diff -func (issue *Issue) DiffURL() string { - if issue.IsPull { - return fmt.Sprintf("%s/pulls/%d.diff", issue.Repo.HTMLURL(), issue.Index) - } - return "" -} - -// PatchURL returns the absolute URL to this patch -func (issue *Issue) PatchURL() string { - if issue.IsPull { - return fmt.Sprintf("%s/pulls/%d.patch", issue.Repo.HTMLURL(), issue.Index) - } - return "" -} - -// State returns string representation of issue status. -func (issue *Issue) State() api.StateType { - if issue.IsClosed { - return api.StateClosed - } - return api.StateOpen -} - -// HashTag returns unique hash tag for issue. -func (issue *Issue) HashTag() string { - return fmt.Sprintf("issue-%d", issue.ID) -} - -// IsPoster returns true if given user by ID is the poster. -func (issue *Issue) IsPoster(uid int64) bool { - return issue.OriginalAuthorID == 0 && issue.PosterID == uid -} - -func (issue *Issue) getLabels(ctx context.Context) (err error) { - if len(issue.Labels) > 0 { - return nil - } - - issue.Labels, err = GetLabelsByIssueID(ctx, issue.ID) - if err != nil { - return fmt.Errorf("getLabelsByIssueID: %v", err) - } - return nil -} - -func clearIssueLabels(ctx context.Context, issue *Issue, doer *user_model.User) (err error) { - if err = issue.getLabels(ctx); err != nil { - return fmt.Errorf("getLabels: %v", err) - } - - for i := range issue.Labels { - if err = deleteIssueLabel(ctx, issue, issue.Labels[i], doer); err != nil { - return fmt.Errorf("removeLabel: %v", err) - } - } - - return nil -} - -// ClearIssueLabels removes all issue labels as the given user. -// Triggers appropriate WebHooks, if any. -func ClearIssueLabels(issue *Issue, doer *user_model.User) (err error) { - ctx, committer, err := db.TxContext() - if err != nil { - return err - } - defer committer.Close() - - if err := issue.LoadRepo(ctx); err != nil { - return err - } else if err = issue.loadPullRequest(ctx); err != nil { - return err - } - - perm, err := access_model.GetUserRepoPermission(ctx, issue.Repo, doer) - if err != nil { - return err - } - if !perm.CanWriteIssuesOrPulls(issue.IsPull) { - return ErrRepoLabelNotExist{} - } - - if err = clearIssueLabels(ctx, issue, doer); err != nil { - return err - } - - if err = committer.Commit(); err != nil { - return fmt.Errorf("Commit: %v", err) - } - - return nil -} - -type labelSorter []*Label - -func (ts labelSorter) Len() int { - return len([]*Label(ts)) -} - -func (ts labelSorter) Less(i, j int) bool { - return []*Label(ts)[i].ID < []*Label(ts)[j].ID -} - -func (ts labelSorter) Swap(i, j int) { - []*Label(ts)[i], []*Label(ts)[j] = []*Label(ts)[j], []*Label(ts)[i] -} - -// ReplaceIssueLabels removes all current labels and add new labels to the issue. -// Triggers appropriate WebHooks, if any. -func ReplaceIssueLabels(issue *Issue, labels []*Label, doer *user_model.User) (err error) { - ctx, committer, err := db.TxContext() - if err != nil { - return err - } - defer committer.Close() - - if err = issue.LoadRepo(ctx); err != nil { - return err - } - - if err = issue.LoadLabels(ctx); err != nil { - return err - } - - sort.Sort(labelSorter(labels)) - sort.Sort(labelSorter(issue.Labels)) - - var toAdd, toRemove []*Label - - addIndex, removeIndex := 0, 0 - for addIndex < len(labels) && removeIndex < len(issue.Labels) { - addLabel := labels[addIndex] - removeLabel := issue.Labels[removeIndex] - if addLabel.ID == removeLabel.ID { - // Silently drop invalid labels - if removeLabel.RepoID != issue.RepoID && removeLabel.OrgID != issue.Repo.OwnerID { - toRemove = append(toRemove, removeLabel) - } - - addIndex++ - removeIndex++ - } else if addLabel.ID < removeLabel.ID { - // Only add if the label is valid - if addLabel.RepoID == issue.RepoID || addLabel.OrgID == issue.Repo.OwnerID { - toAdd = append(toAdd, addLabel) - } - addIndex++ - } else { - toRemove = append(toRemove, removeLabel) - removeIndex++ - } - } - toAdd = append(toAdd, labels[addIndex:]...) - toRemove = append(toRemove, issue.Labels[removeIndex:]...) - - if len(toAdd) > 0 { - if err = newIssueLabels(ctx, issue, toAdd, doer); err != nil { - return fmt.Errorf("addLabels: %v", err) - } - } - - for _, l := range toRemove { - if err = deleteIssueLabel(ctx, issue, l, doer); err != nil { - return fmt.Errorf("removeLabel: %v", err) - } - } - - issue.Labels = nil - if err = issue.LoadLabels(ctx); err != nil { - return err - } - - 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 { - return err - } - return nil -} - -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) - if err != nil { - return nil, err - } - - // Nothing should be performed if current status is same as target status - if currentIssue.IsClosed == isClosed { - if !issue.IsPull { - return nil, ErrIssueWasClosed{ - ID: issue.ID, - } - } - return nil, ErrPullWasClosed{ - ID: issue.ID, - } - } - - issue.IsClosed = isClosed - return doChangeIssueStatus(ctx, issue, doer, isMergePull) -} - -func doChangeIssueStatus(ctx context.Context, issue *Issue, doer *user_model.User, isMergePull bool) (*Comment, error) { - // Check for open dependencies - if issue.IsClosed && issue.Repo.IsDependenciesEnabledCtx(ctx) { - // only check if dependencies are enabled and we're about to close an issue, otherwise reopening an issue would fail when there are unsatisfied dependencies - noDeps, err := IssueNoDependenciesLeft(ctx, issue) - if err != nil { - return nil, err - } - - if !noDeps { - return nil, ErrDependenciesLeft{issue.ID} - } - } - - if issue.IsClosed { - issue.ClosedUnix = timeutil.TimeStampNow() - } else { - issue.ClosedUnix = 0 - } - - if err := UpdateIssueCols(ctx, issue, "is_closed", "closed_unix"); err != nil { - return nil, err - } - - // Update issue count of labels - if err := issue.getLabels(ctx); err != nil { - return nil, err - } - for idx := range issue.Labels { - if err := updateLabelCols(ctx, issue.Labels[idx], "num_issues", "num_closed_issue"); err != nil { - return nil, err - } - } - - // Update issue count of milestone - if issue.MilestoneID > 0 { - if err := issues_model.UpdateMilestoneCounters(ctx, issue.MilestoneID); err != nil { - return nil, err - } - } - - if err := updateIssueClosedNum(ctx, issue); err != nil { - return nil, err - } - - // New action comment - cmtType := CommentTypeClose - if !issue.IsClosed { - cmtType = CommentTypeReopen - } else if isMergePull { - cmtType = CommentTypeMergePull - } - - return CreateCommentCtx(ctx, &CreateCommentOptions{ - Type: cmtType, - Doer: doer, - Repo: issue.Repo, - Issue: issue, - }) -} - -// ChangeIssueStatus changes issue status to open or closed. -func ChangeIssueStatus(ctx context.Context, issue *Issue, doer *user_model.User, isClosed bool) (*Comment, error) { - if err := issue.LoadRepo(ctx); err != nil { - return nil, err - } - if err := issue.loadPoster(ctx); err != nil { - return nil, err - } - - return changeIssueStatus(ctx, issue, doer, isClosed, false) -} - -// ChangeIssueTitle changes the title of this issue, as the given user. -func ChangeIssueTitle(issue *Issue, doer *user_model.User, oldTitle string) (err error) { - ctx, committer, err := db.TxContext() - if err != nil { - return err - } - defer committer.Close() - - if err = UpdateIssueCols(ctx, issue, "name"); err != nil { - return fmt.Errorf("updateIssueCols: %v", err) - } - - if err = issue.LoadRepo(ctx); err != nil { - return fmt.Errorf("loadRepo: %v", err) - } - - opts := &CreateCommentOptions{ - Type: CommentTypeChangeTitle, - Doer: doer, - Repo: issue.Repo, - Issue: issue, - OldTitle: oldTitle, - NewTitle: issue.Title, - } - if _, err = CreateCommentCtx(ctx, opts); err != nil { - return fmt.Errorf("createComment: %v", err) - } - if err = issue.addCrossReferences(ctx, doer, true); err != nil { - return err - } - - return committer.Commit() -} - -// ChangeIssueRef changes the branch of this issue, as the given user. -func ChangeIssueRef(issue *Issue, doer *user_model.User, oldRef string) (err error) { - ctx, committer, err := db.TxContext() - if err != nil { - return err - } - defer committer.Close() - - if err = UpdateIssueCols(ctx, issue, "ref"); err != nil { - return fmt.Errorf("updateIssueCols: %v", err) - } - - if err = issue.LoadRepo(ctx); err != nil { - return fmt.Errorf("loadRepo: %v", err) - } - oldRefFriendly := strings.TrimPrefix(oldRef, git.BranchPrefix) - newRefFriendly := strings.TrimPrefix(issue.Ref, git.BranchPrefix) - - opts := &CreateCommentOptions{ - Type: CommentTypeChangeIssueRef, - Doer: doer, - Repo: issue.Repo, - Issue: issue, - OldRef: oldRefFriendly, - NewRef: newRefFriendly, - } - if _, err = CreateCommentCtx(ctx, opts); err != nil { - return fmt.Errorf("createComment: %v", err) - } - - return committer.Commit() -} - -// 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) - if err != nil { - return err - } - opts := &CreateCommentOptions{ - Type: CommentTypeDeleteBranch, - Doer: doer, - Repo: repo, - Issue: issue, - OldRef: branchName, - } - _, err = CreateCommentCtx(ctx, opts) - return err -} - -// UpdateIssueAttachments update attachments by UUIDs for the issue -func UpdateIssueAttachments(issueID int64, uuids []string) (err error) { - ctx, committer, err := db.TxContext() - if err != nil { - return err - } - defer committer.Close() - attachments, err := repo_model.GetAttachmentsByUUIDs(ctx, uuids) - if err != nil { - return fmt.Errorf("getAttachmentsByUUIDs [uuids: %v]: %v", uuids, err) - } - for i := 0; i < len(attachments); i++ { - attachments[i].IssueID = issueID - if err := repo_model.UpdateAttachment(ctx, attachments[i]); err != nil { - return fmt.Errorf("update attachment [id: %d]: %v", attachments[i].ID, err) - } - } - return committer.Commit() -} - -// ChangeIssueContent changes issue content, as the given user. -func ChangeIssueContent(issue *Issue, doer *user_model.User, content string) (err error) { - ctx, committer, err := db.TxContext() - if err != nil { - return err - } - defer committer.Close() - - hasContentHistory, err := issues_model.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, - issue.CreatedUnix, issue.Content, true); err != nil { - return fmt.Errorf("SaveIssueContentHistory: %v", err) - } - } - - issue.Content = content - - if err = UpdateIssueCols(ctx, issue, "content"); err != nil { - return fmt.Errorf("UpdateIssueCols: %v", err) - } - - if err = issues_model.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 { - return fmt.Errorf("addCrossReferences: %v", err) - } - - return committer.Commit() -} - -// GetTasks returns the amount of tasks in the issues content -func (issue *Issue) GetTasks() int { - return len(issueTasksPat.FindAllStringIndex(issue.Content, -1)) -} - -// GetTasksDone returns the amount of completed tasks in the issues content -func (issue *Issue) GetTasksDone() int { - return len(issueTasksDonePat.FindAllStringIndex(issue.Content, -1)) -} - -// GetLastEventTimestamp returns the last user visible event timestamp, either the creation of this issue or the close. -func (issue *Issue) GetLastEventTimestamp() timeutil.TimeStamp { - if issue.IsClosed { - return issue.ClosedUnix - } - return issue.CreatedUnix -} - -// GetLastEventLabel returns the localization label for the current issue. -func (issue *Issue) GetLastEventLabel() string { - if issue.IsClosed { - if issue.IsPull && issue.PullRequest.HasMerged { - return "repo.pulls.merged_by" - } - return "repo.issues.closed_by" - } - return "repo.issues.opened_by" -} - -// GetLastComment return last comment for the current issue. -func (issue *Issue) GetLastComment() (*Comment, error) { - var c Comment - exist, err := db.GetEngine(db.DefaultContext).Where("type = ?", CommentTypeComment). - And("issue_id = ?", issue.ID).Desc("created_unix").Get(&c) - if err != nil { - return nil, err - } - if !exist { - return nil, nil - } - return &c, nil -} - -// GetLastEventLabelFake returns the localization label for the current issue without providing a link in the username. -func (issue *Issue) GetLastEventLabelFake() string { - if issue.IsClosed { - if issue.IsPull && issue.PullRequest.HasMerged { - return "repo.pulls.merged_by_fake" - } - return "repo.issues.closed_by_fake" - } - return "repo.issues.opened_by_fake" -} - -// NewIssueOptions represents the options of a new issue. -type NewIssueOptions struct { - Repo *repo_model.Repository - Issue *Issue - LabelIDs []int64 - Attachments []string // In UUID format. - IsPull bool -} - -func newIssue(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) { - return fmt.Errorf("getMilestoneByID: %v", err) - } - - // Assume milestone is invalid and drop silently. - opts.Issue.MilestoneID = 0 - if milestone != nil { - opts.Issue.MilestoneID = milestone.ID - opts.Issue.Milestone = milestone - } - } - - if opts.Issue.Index <= 0 { - return fmt.Errorf("no issue index provided") - } - if opts.Issue.ID > 0 { - return fmt.Errorf("issue exist") - } - - if _, err := e.Insert(opts.Issue); err != nil { - return err - } - - if opts.Issue.MilestoneID > 0 { - if err := issues_model.UpdateMilestoneCounters(ctx, opts.Issue.MilestoneID); err != nil { - return err - } - - opts := &CreateCommentOptions{ - Type: CommentTypeMilestone, - Doer: doer, - Repo: opts.Repo, - Issue: opts.Issue, - OldMilestoneID: 0, - MilestoneID: opts.Issue.MilestoneID, - } - if _, err = CreateCommentCtx(ctx, opts); err != nil { - return err - } - } - - if opts.IsPull { - _, err = e.Exec("UPDATE `repository` SET num_pulls = num_pulls + 1 WHERE id = ?", opts.Issue.RepoID) - } else { - _, err = e.Exec("UPDATE `repository` SET num_issues = num_issues + 1 WHERE id = ?", opts.Issue.RepoID) - } - if err != nil { - return err - } - - if len(opts.LabelIDs) > 0 { - // During the session, SQLite3 driver cannot handle retrieve objects after update something. - // So we have to get all needed labels first. - labels := make([]*Label, 0, len(opts.LabelIDs)) - if err = e.In("id", opts.LabelIDs).Find(&labels); err != nil { - return fmt.Errorf("find all labels [label_ids: %v]: %v", opts.LabelIDs, err) - } - - if err = opts.Issue.loadPoster(ctx); err != nil { - return err - } - - for _, label := range labels { - // Silently drop invalid labels. - if label.RepoID != opts.Repo.ID && label.OrgID != opts.Repo.OwnerID { - continue - } - - if err = newIssueLabel(ctx, opts.Issue, label, opts.Issue.Poster); err != nil { - return fmt.Errorf("addLabel [id: %d]: %v", label.ID, err) - } - } - } - - if err = newIssueUsers(ctx, opts.Repo, opts.Issue); err != nil { - return err - } - - if len(opts.Attachments) > 0 { - attachments, err := repo_model.GetAttachmentsByUUIDs(ctx, opts.Attachments) - if err != nil { - return fmt.Errorf("getAttachmentsByUUIDs [uuids: %v]: %v", opts.Attachments, err) - } - - for i := 0; i < len(attachments); i++ { - attachments[i].IssueID = opts.Issue.ID - if _, err = e.ID(attachments[i].ID).Update(attachments[i]); err != nil { - return fmt.Errorf("update attachment [id: %d]: %v", attachments[i].ID, err) - } - } - } - 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() -} - -// NewIssue creates new issue with labels for repository. -func NewIssue(repo *repo_model.Repository, issue *Issue, labelIDs []int64, uuids []string) (err error) { - idx, err := db.GetNextResourceIndex("issue_index", repo.ID) - if err != nil { - return fmt.Errorf("generate issue index failed: %v", err) - } - - issue.Index = idx - - ctx, committer, err := db.TxContext() - if err != nil { - return err - } - defer committer.Close() - - if err = newIssue(ctx, issue.Poster, NewIssueOptions{ - Repo: repo, - Issue: issue, - LabelIDs: labelIDs, - Attachments: uuids, - }); err != nil { - if IsErrUserDoesNotHaveAccessToRepo(err) || IsErrNewIssueInsert(err) { - return err - } - return fmt.Errorf("newIssue: %v", err) - } - - if err = committer.Commit(); err != nil { - return fmt.Errorf("Commit: %v", err) - } - - return nil -} - -// GetIssueByIndex returns raw issue without loading attributes by index in a repository. -func GetIssueByIndex(repoID, index int64) (*Issue, error) { - if index < 1 { - return nil, ErrIssueNotExist{} - } - issue := &Issue{ - RepoID: repoID, - Index: index, - } - has, err := db.GetEngine(db.DefaultContext).Get(issue) - if err != nil { - return nil, err - } else if !has { - return nil, ErrIssueNotExist{0, repoID, index} - } - return issue, nil -} - -// GetIssueByForeignIndex returns raw issue by foreign ID -func GetIssueByForeignIndex(ctx context.Context, repoID, foreignIndex int64) (*Issue, error) { - reference := &foreignreference.ForeignReference{ - RepoID: repoID, - ForeignIndex: strconv.FormatInt(foreignIndex, 10), - Type: foreignreference.TypeIssue, - } - has, err := db.GetEngine(ctx).Get(reference) - if err != nil { - return nil, err - } else if !has { - return nil, foreignreference.ErrLocalIndexNotExist{ - RepoID: repoID, - ForeignIndex: foreignIndex, - Type: foreignreference.TypeIssue, - } - } - return GetIssueByIndex(repoID, reference.LocalIndex) -} - -// GetIssueWithAttrsByIndex returns issue by index in a repository. -func GetIssueWithAttrsByIndex(repoID, index int64) (*Issue, error) { - issue, err := GetIssueByIndex(repoID, index) - if err != nil { - return nil, err - } - return issue, issue.LoadAttributes() -} - -func getIssueByID(ctx context.Context, id int64) (*Issue, error) { - issue := new(Issue) - has, err := db.GetEngine(ctx).ID(id).Get(issue) - if err != nil { - return nil, err - } else if !has { - return nil, ErrIssueNotExist{id, 0, 0} - } - return issue, nil -} - -// GetIssueWithAttrsByID returns an issue with attributes by given ID. -func GetIssueWithAttrsByID(id int64) (*Issue, error) { - 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) -} - -// GetIssuesByIDs return issues with the given IDs. -func GetIssuesByIDs(ctx context.Context, issueIDs []int64) ([]*Issue, error) { - issues := make([]*Issue, 0, 10) - return issues, db.GetEngine(ctx).In("id", issueIDs).Find(&issues) -} - -// GetIssueIDsByRepoID returns all issue ids by repo id -func GetIssueIDsByRepoID(ctx context.Context, repoID int64) ([]int64, error) { - ids := make([]int64, 0, 10) - err := db.GetEngine(ctx).Table("issue").Cols("id").Where("repo_id = ?", repoID).Find(&ids) - return ids, err -} - -// IssuesOptions represents options of an issue. -type IssuesOptions struct { - db.ListOptions - RepoID int64 // overwrites RepoCond if not 0 - RepoCond builder.Cond - AssigneeID int64 - PosterID int64 - MentionedID int64 - ReviewRequestedID int64 - MilestoneIDs []int64 - ProjectID int64 - ProjectBoardID int64 - IsClosed util.OptionalBool - IsPull util.OptionalBool - LabelIDs []int64 - IncludedLabelNames []string - ExcludedLabelNames []string - IncludeMilestones []string - SortType string - IssueIDs []int64 - UpdatedAfterUnix int64 - UpdatedBeforeUnix int64 - // prioritize issues from this repo - PriorityRepoID int64 - IsArchived util.OptionalBool - Org *organization.Organization // issues permission scope - Team *organization.Team // issues permission scope - User *user_model.User // issues permission scope -} - -// sortIssuesSession sort an issues-related session based on the provided -// sortType string -func sortIssuesSession(sess *xorm.Session, sortType string, priorityRepoID int64) { - switch sortType { - case "oldest": - sess.Asc("issue.created_unix").Asc("issue.id") - case "recentupdate": - sess.Desc("issue.updated_unix").Desc("issue.created_unix").Desc("issue.id") - case "leastupdate": - sess.Asc("issue.updated_unix").Asc("issue.created_unix").Asc("issue.id") - case "mostcomment": - sess.Desc("issue.num_comments").Desc("issue.created_unix").Desc("issue.id") - case "leastcomment": - sess.Asc("issue.num_comments").Desc("issue.created_unix").Desc("issue.id") - case "priority": - sess.Desc("issue.priority").Desc("issue.created_unix").Desc("issue.id") - case "nearduedate": - // 253370764800 is 01/01/9999 @ 12:00am (UTC) - sess.Join("LEFT", "milestone", "issue.milestone_id = milestone.id"). - OrderBy("CASE " + - "WHEN issue.deadline_unix = 0 AND (milestone.deadline_unix = 0 OR milestone.deadline_unix IS NULL) THEN 253370764800 " + - "WHEN milestone.deadline_unix = 0 OR milestone.deadline_unix IS NULL THEN issue.deadline_unix " + - "WHEN milestone.deadline_unix < issue.deadline_unix OR issue.deadline_unix = 0 THEN milestone.deadline_unix " + - "ELSE issue.deadline_unix END ASC"). - Desc("issue.created_unix"). - Desc("issue.id") - case "farduedate": - sess.Join("LEFT", "milestone", "issue.milestone_id = milestone.id"). - OrderBy("CASE " + - "WHEN milestone.deadline_unix IS NULL THEN issue.deadline_unix " + - "WHEN milestone.deadline_unix < issue.deadline_unix OR issue.deadline_unix = 0 THEN milestone.deadline_unix " + - "ELSE issue.deadline_unix END DESC"). - Desc("issue.created_unix"). - Desc("issue.id") - case "priorityrepo": - sess.OrderBy("CASE "+ - "WHEN issue.repo_id = ? THEN 1 "+ - "ELSE 2 END ASC", priorityRepoID). - Desc("issue.created_unix"). - Desc("issue.id") - case "project-column-sorting": - sess.Asc("project_issue.sorting").Desc("issue.created_unix").Desc("issue.id") - default: - sess.Desc("issue.created_unix").Desc("issue.id") - } -} - -func (opts *IssuesOptions) setupSessionWithLimit(sess *xorm.Session) { - if opts.Page >= 0 && opts.PageSize > 0 { - var start int - if opts.Page == 0 { - start = 0 - } else { - start = (opts.Page - 1) * opts.PageSize - } - sess.Limit(opts.PageSize, start) - } - opts.setupSessionNoLimit(sess) -} - -func (opts *IssuesOptions) setupSessionNoLimit(sess *xorm.Session) { - if len(opts.IssueIDs) > 0 { - sess.In("issue.id", opts.IssueIDs) - } - - if opts.RepoID != 0 { - opts.RepoCond = builder.Eq{"issue.repo_id": opts.RepoID} - } - if opts.RepoCond != nil { - sess.And(opts.RepoCond) - } - - if !opts.IsClosed.IsNone() { - sess.And("issue.is_closed=?", opts.IsClosed.IsTrue()) - } - - if opts.AssigneeID > 0 { - applyAssigneeCondition(sess, opts.AssigneeID) - } - - if opts.PosterID > 0 { - applyPosterCondition(sess, opts.PosterID) - } - - if opts.MentionedID > 0 { - applyMentionedCondition(sess, opts.MentionedID) - } - - if opts.ReviewRequestedID > 0 { - applyReviewRequestedCondition(sess, opts.ReviewRequestedID) - } - - if len(opts.MilestoneIDs) > 0 { - sess.In("issue.milestone_id", opts.MilestoneIDs) - } - - if opts.UpdatedAfterUnix != 0 { - sess.And(builder.Gte{"issue.updated_unix": opts.UpdatedAfterUnix}) - } - if opts.UpdatedBeforeUnix != 0 { - sess.And(builder.Lte{"issue.updated_unix": opts.UpdatedBeforeUnix}) - } - - if opts.ProjectID > 0 { - sess.Join("INNER", "project_issue", "issue.id = project_issue.issue_id"). - And("project_issue.project_id=?", opts.ProjectID) - } - - if opts.ProjectBoardID != 0 { - if opts.ProjectBoardID > 0 { - sess.In("issue.id", builder.Select("issue_id").From("project_issue").Where(builder.Eq{"project_board_id": opts.ProjectBoardID})) - } else { - sess.In("issue.id", builder.Select("issue_id").From("project_issue").Where(builder.Eq{"project_board_id": 0})) - } - } - - switch opts.IsPull { - case util.OptionalBoolTrue: - sess.And("issue.is_pull=?", true) - case util.OptionalBoolFalse: - sess.And("issue.is_pull=?", false) - } - - if opts.IsArchived != util.OptionalBoolNone { - sess.And(builder.Eq{"repository.is_archived": opts.IsArchived.IsTrue()}) - } - - if opts.LabelIDs != nil { - for i, labelID := range opts.LabelIDs { - if labelID > 0 { - sess.Join("INNER", fmt.Sprintf("issue_label il%d", i), - fmt.Sprintf("issue.id = il%[1]d.issue_id AND il%[1]d.label_id = %[2]d", i, labelID)) - } else { - sess.Where("issue.id not in (select issue_id from issue_label where label_id = ?)", -labelID) - } - } - } - - if len(opts.IncludedLabelNames) > 0 { - sess.In("issue.id", BuildLabelNamesIssueIDsCondition(opts.IncludedLabelNames)) - } - - if len(opts.ExcludedLabelNames) > 0 { - sess.And(builder.NotIn("issue.id", BuildLabelNamesIssueIDsCondition(opts.ExcludedLabelNames))) - } - - if len(opts.IncludeMilestones) > 0 { - sess.In("issue.milestone_id", - builder.Select("id"). - From("milestone"). - Where(builder.In("name", opts.IncludeMilestones))) - } - - if opts.User != nil { - sess.And(issuePullAccessibleRepoCond("issue.repo_id", opts.User.ID, opts.Org, opts.Team, opts.IsPull.IsTrue())) - } -} - -// teamUnitsRepoCond returns query condition for those repo id in the special org team with special units access -func teamUnitsRepoCond(id string, userID, orgID, teamID int64, units ...unit.Type) builder.Cond { - return builder.In(id, - builder.Select("repo_id").From("team_repo").Where( - builder.Eq{ - "team_id": teamID, - }.And( - builder.Or( - // Check if the user is member of the team. - builder.In( - "team_id", builder.Select("team_id").From("team_user").Where( - builder.Eq{ - "uid": userID, - }, - ), - ), - // Check if the user is in the owner team of the organisation. - builder.Exists(builder.Select("team_id").From("team_user"). - Where(builder.Eq{ - "org_id": orgID, - "team_id": builder.Select("id").From("team").Where( - builder.Eq{ - "org_id": orgID, - "lower_name": strings.ToLower(organization.OwnerTeamName), - }), - "uid": userID, - }), - ), - )).And( - builder.In( - "team_id", builder.Select("team_id").From("team_unit").Where( - builder.Eq{ - "`team_unit`.org_id": orgID, - }.And( - builder.In("`team_unit`.type", units), - ), - ), - ), - ), - )) -} - -// issuePullAccessibleRepoCond userID must not be zero, this condition require join repository table -func issuePullAccessibleRepoCond(repoIDstr string, userID int64, org *organization.Organization, team *organization.Team, isPull bool) builder.Cond { - cond := builder.NewCond() - unitType := unit.TypeIssues - if isPull { - unitType = unit.TypePullRequests - } - if org != nil { - if team != nil { - cond = cond.And(teamUnitsRepoCond(repoIDstr, userID, org.ID, team.ID, unitType)) // special team member repos - } else { - cond = cond.And( - builder.Or( - repo_model.UserOrgUnitRepoCond(repoIDstr, userID, org.ID, unitType), // team member repos - repo_model.UserOrgPublicUnitRepoCond(userID, org.ID), // user org public non-member repos, TODO: check repo has issues - ), - ) - } - } else { - cond = cond.And( - builder.Or( - repo_model.UserOwnedRepoCond(userID), // owned repos - repo_model.UserCollaborationRepoCond(repoIDstr, userID), // collaboration repos - repo_model.UserAssignedRepoCond(repoIDstr, userID), // user has been assigned accessible public repos - repo_model.UserMentionedRepoCond(repoIDstr, userID), // user has been mentioned accessible public repos - repo_model.UserCreateIssueRepoCond(repoIDstr, userID, isPull), // user has created issue/pr accessible public repos - ), - ) - } - return cond -} - -func applyAssigneeCondition(sess *xorm.Session, assigneeID int64) *xorm.Session { - return sess.Join("INNER", "issue_assignees", "issue.id = issue_assignees.issue_id"). - And("issue_assignees.assignee_id = ?", assigneeID) -} - -func applyPosterCondition(sess *xorm.Session, posterID int64) *xorm.Session { - return sess.And("issue.poster_id=?", posterID) -} - -func applyMentionedCondition(sess *xorm.Session, mentionedID int64) *xorm.Session { - return sess.Join("INNER", "issue_user", "issue.id = issue_user.issue_id"). - And("issue_user.is_mentioned = ?", true). - And("issue_user.uid = ?", mentionedID) -} - -func applyReviewRequestedCondition(sess *xorm.Session, reviewRequestedID int64) *xorm.Session { - return sess.Join("INNER", []string{"review", "r"}, "issue.id = r.issue_id"). - And("issue.poster_id <> ?", reviewRequestedID). - And("r.type = ?", ReviewTypeRequest). - And("r.reviewer_id = ? and r.id in (select max(id) from review where issue_id = r.issue_id and reviewer_id = r.reviewer_id and type in (?, ?, ?))"+ - " or r.reviewer_team_id in (select team_id from team_user where uid = ?)", - reviewRequestedID, ReviewTypeApprove, ReviewTypeReject, ReviewTypeRequest, reviewRequestedID) -} - -// CountIssuesByRepo map from repoID to number of issues matching the options -func CountIssuesByRepo(opts *IssuesOptions) (map[int64]int64, error) { - e := db.GetEngine(db.DefaultContext) - - sess := e.Join("INNER", "repository", "`issue`.repo_id = `repository`.id") - - opts.setupSessionNoLimit(sess) - - countsSlice := make([]*struct { - RepoID int64 - Count int64 - }, 0, 10) - if err := sess.GroupBy("issue.repo_id"). - Select("issue.repo_id AS repo_id, COUNT(*) AS count"). - Table("issue"). - Find(&countsSlice); err != nil { - return nil, fmt.Errorf("unable to CountIssuesByRepo: %w", err) - } - - countMap := make(map[int64]int64, len(countsSlice)) - for _, c := range countsSlice { - countMap[c.RepoID] = c.Count - } - return countMap, nil -} - -// GetRepoIDsForIssuesOptions find all repo ids for the given options -func GetRepoIDsForIssuesOptions(opts *IssuesOptions, user *user_model.User) ([]int64, error) { - repoIDs := make([]int64, 0, 5) - e := db.GetEngine(db.DefaultContext) - - sess := e.Join("INNER", "repository", "`issue`.repo_id = `repository`.id") - - opts.setupSessionNoLimit(sess) - - accessCond := repo_model.AccessibleRepositoryCondition(user) - if err := sess.Where(accessCond). - Distinct("issue.repo_id"). - Table("issue"). - Find(&repoIDs); err != nil { - return nil, fmt.Errorf("unable to GetRepoIDsForIssuesOptions: %w", err) - } - - return repoIDs, nil -} - -// Issues returns a list of issues by given conditions. -func Issues(opts *IssuesOptions) ([]*Issue, error) { - e := db.GetEngine(db.DefaultContext) - - sess := e.Join("INNER", "repository", "`issue`.repo_id = `repository`.id") - opts.setupSessionWithLimit(sess) - - sortIssuesSession(sess, opts.SortType, opts.PriorityRepoID) - - issues := make([]*Issue, 0, opts.ListOptions.PageSize) - if err := sess.Find(&issues); err != nil { - return nil, fmt.Errorf("unable to query Issues: %w", err) - } - - if err := IssueList(issues).LoadAttributes(); err != nil { - return nil, fmt.Errorf("unable to LoadAttributes for Issues: %w", err) - } - - return issues, nil -} - -// CountIssues number return of issues by given conditions. -func CountIssues(opts *IssuesOptions) (int64, error) { - e := db.GetEngine(db.DefaultContext) - - sess := e.Select("COUNT(issue.id) AS count").Table("issue") - sess.Join("INNER", "repository", "`issue`.repo_id = `repository`.id") - opts.setupSessionNoLimit(sess) - - return sess.Count() -} - -// GetParticipantsIDsByIssueID returns the IDs of all users who participated in comments of an issue, -// but skips joining with `user` for performance reasons. -// User permissions must be verified elsewhere if required. -func GetParticipantsIDsByIssueID(issueID int64) ([]int64, error) { - userIDs := make([]int64, 0, 5) - return userIDs, db.GetEngine(db.DefaultContext).Table("comment"). - Cols("poster_id"). - Where("issue_id = ?", issueID). - And("type in (?,?,?)", CommentTypeComment, CommentTypeCode, CommentTypeReview). - Distinct("poster_id"). - Find(&userIDs) -} - -// 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) - if err != nil { - log.Error(err.Error()) - return false - } - return util.IsInt64InSlice(user.ID, userIDs) -} - -// UpdateIssueMentions updates issue-user relations for mentioned users. -func UpdateIssueMentions(ctx context.Context, issueID int64, mentions []*user_model.User) error { - if len(mentions) == 0 { - return nil - } - ids := make([]int64, len(mentions)) - for i, u := range mentions { - ids[i] = u.ID - } - if err := UpdateIssueUsersByMentions(ctx, issueID, ids); err != nil { - return fmt.Errorf("UpdateIssueUsersByMentions: %v", err) - } - return nil -} - -// IssueStats represents issue statistic information. -type IssueStats struct { - OpenCount, ClosedCount int64 - YourRepositoriesCount int64 - AssignCount int64 - CreateCount int64 - MentionCount int64 - ReviewRequestedCount int64 -} - -// Filter modes. -const ( - FilterModeAll = iota - FilterModeAssign - FilterModeCreate - FilterModeMention - FilterModeReviewRequested - 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 - Labels string - MilestoneID int64 - AssigneeID int64 - MentionedID int64 - PosterID int64 - ReviewRequestedID int64 - IsPull util.OptionalBool - IssueIDs []int64 -} - -const ( - // When queries are broken down in parts because of the number - // of parameters, attempt to break by this amount - maxQueryParameters = 300 -) - -// GetIssueStats returns issue statistic information by given conditions. -func GetIssueStats(opts *IssueStatsOptions) (*IssueStats, error) { - if len(opts.IssueIDs) <= maxQueryParameters { - return getIssueStatsChunk(opts, opts.IssueIDs) - } - - // If too long a list of IDs is provided, we get the statistics in - // smaller chunks and get accumulates. Note: this could potentially - // get us invalid results. The alternative is to insert the list of - // ids in a temporary table and join from them. - accum := &IssueStats{} - for i := 0; i < len(opts.IssueIDs); { - chunk := i + maxQueryParameters - if chunk > len(opts.IssueIDs) { - chunk = len(opts.IssueIDs) - } - stats, err := getIssueStatsChunk(opts, opts.IssueIDs[i:chunk]) - if err != nil { - return nil, err - } - accum.OpenCount += stats.OpenCount - accum.ClosedCount += stats.ClosedCount - accum.YourRepositoriesCount += stats.YourRepositoriesCount - accum.AssignCount += stats.AssignCount - accum.CreateCount += stats.CreateCount - accum.OpenCount += stats.MentionCount - accum.ReviewRequestedCount += stats.ReviewRequestedCount - i = chunk - } - return accum, nil -} - -func getIssueStatsChunk(opts *IssueStatsOptions, issueIDs []int64) (*IssueStats, error) { - stats := &IssueStats{} - - countSession := func(opts *IssueStatsOptions, issueIDs []int64) *xorm.Session { - sess := db.GetEngine(db.DefaultContext). - Where("issue.repo_id = ?", opts.RepoID) - - if len(issueIDs) > 0 { - sess.In("issue.id", issueIDs) - } - - if len(opts.Labels) > 0 && opts.Labels != "0" { - labelIDs, err := base.StringsToInt64s(strings.Split(opts.Labels, ",")) - if err != nil { - log.Warn("Malformed Labels argument: %s", opts.Labels) - } else { - for i, labelID := range labelIDs { - if labelID > 0 { - sess.Join("INNER", fmt.Sprintf("issue_label il%d", i), - fmt.Sprintf("issue.id = il%[1]d.issue_id AND il%[1]d.label_id = %[2]d", i, labelID)) - } else { - sess.Where("issue.id NOT IN (SELECT issue_id FROM issue_label WHERE label_id = ?)", -labelID) - } - } - } - } - - if opts.MilestoneID > 0 { - sess.And("issue.milestone_id = ?", opts.MilestoneID) - } - - if opts.AssigneeID > 0 { - applyAssigneeCondition(sess, opts.AssigneeID) - } - - if opts.PosterID > 0 { - applyPosterCondition(sess, opts.PosterID) - } - - if opts.MentionedID > 0 { - applyMentionedCondition(sess, opts.MentionedID) - } - - if opts.ReviewRequestedID > 0 { - applyReviewRequestedCondition(sess, opts.ReviewRequestedID) - } - - switch opts.IsPull { - case util.OptionalBoolTrue: - sess.And("issue.is_pull=?", true) - case util.OptionalBoolFalse: - sess.And("issue.is_pull=?", false) - } - - return sess - } - - var err error - stats.OpenCount, err = countSession(opts, issueIDs). - And("issue.is_closed = ?", false). - Count(new(Issue)) - if err != nil { - return stats, err - } - stats.ClosedCount, err = countSession(opts, issueIDs). - And("issue.is_closed = ?", true). - Count(new(Issue)) - return stats, err -} - -// UserIssueStatsOptions contains parameters accepted by GetUserIssueStats. -type UserIssueStatsOptions struct { - UserID int64 - RepoIDs []int64 - FilterMode int - IsPull bool - IsClosed bool - IssueIDs []int64 - IsArchived util.OptionalBool - LabelIDs []int64 - RepoCond builder.Cond - Org *organization.Organization - Team *organization.Team -} - -// GetUserIssueStats returns issue statistic information for dashboard by given conditions. -func GetUserIssueStats(opts UserIssueStatsOptions) (*IssueStats, error) { - var err error - stats := &IssueStats{} - - cond := builder.NewCond() - cond = cond.And(builder.Eq{"issue.is_pull": opts.IsPull}) - if len(opts.RepoIDs) > 0 { - cond = cond.And(builder.In("issue.repo_id", opts.RepoIDs)) - } - if len(opts.IssueIDs) > 0 { - cond = cond.And(builder.In("issue.id", opts.IssueIDs)) - } - if opts.RepoCond != nil { - cond = cond.And(opts.RepoCond) - } - - if opts.UserID > 0 { - cond = cond.And(issuePullAccessibleRepoCond("issue.repo_id", opts.UserID, opts.Org, opts.Team, opts.IsPull)) - } - - sess := func(cond builder.Cond) *xorm.Session { - s := db.GetEngine(db.DefaultContext).Where(cond) - if len(opts.LabelIDs) > 0 { - s.Join("INNER", "issue_label", "issue_label.issue_id = issue.id"). - In("issue_label.label_id", opts.LabelIDs) - } - if opts.UserID > 0 || opts.IsArchived != util.OptionalBoolNone { - s.Join("INNER", "repository", "issue.repo_id = repository.id") - if opts.IsArchived != util.OptionalBoolNone { - s.And(builder.Eq{"repository.is_archived": opts.IsArchived.IsTrue()}) - } - } - return s - } - - switch opts.FilterMode { - case FilterModeAll, FilterModeYourRepositories: - stats.OpenCount, err = sess(cond). - And("issue.is_closed = ?", false). - Count(new(Issue)) - if err != nil { - return nil, err - } - stats.ClosedCount, err = sess(cond). - And("issue.is_closed = ?", true). - Count(new(Issue)) - if err != nil { - return nil, err - } - case FilterModeAssign: - stats.OpenCount, err = applyAssigneeCondition(sess(cond), opts.UserID). - And("issue.is_closed = ?", false). - Count(new(Issue)) - if err != nil { - return nil, err - } - stats.ClosedCount, err = applyAssigneeCondition(sess(cond), opts.UserID). - And("issue.is_closed = ?", true). - Count(new(Issue)) - if err != nil { - return nil, err - } - case FilterModeCreate: - stats.OpenCount, err = applyPosterCondition(sess(cond), opts.UserID). - And("issue.is_closed = ?", false). - Count(new(Issue)) - if err != nil { - return nil, err - } - stats.ClosedCount, err = applyPosterCondition(sess(cond), opts.UserID). - And("issue.is_closed = ?", true). - Count(new(Issue)) - if err != nil { - return nil, err - } - case FilterModeMention: - stats.OpenCount, err = applyMentionedCondition(sess(cond), opts.UserID). - And("issue.is_closed = ?", false). - Count(new(Issue)) - if err != nil { - return nil, err - } - stats.ClosedCount, err = applyMentionedCondition(sess(cond), opts.UserID). - And("issue.is_closed = ?", true). - Count(new(Issue)) - if err != nil { - return nil, err - } - case FilterModeReviewRequested: - stats.OpenCount, err = applyReviewRequestedCondition(sess(cond), opts.UserID). - And("issue.is_closed = ?", false). - Count(new(Issue)) - if err != nil { - return nil, err - } - stats.ClosedCount, err = applyReviewRequestedCondition(sess(cond), opts.UserID). - And("issue.is_closed = ?", true). - Count(new(Issue)) - if err != nil { - return nil, err - } - } - - cond = cond.And(builder.Eq{"issue.is_closed": opts.IsClosed}) - stats.AssignCount, err = applyAssigneeCondition(sess(cond), opts.UserID).Count(new(Issue)) - if err != nil { - return nil, err - } - - stats.CreateCount, err = applyPosterCondition(sess(cond), opts.UserID).Count(new(Issue)) - if err != nil { - return nil, err - } - - stats.MentionCount, err = applyMentionedCondition(sess(cond), opts.UserID).Count(new(Issue)) - if err != nil { - return nil, err - } - - stats.YourRepositoriesCount, err = sess(cond).Count(new(Issue)) - if err != nil { - return nil, err - } - - stats.ReviewRequestedCount, err = applyReviewRequestedCondition(sess(cond), opts.UserID).Count(new(Issue)) - if err != nil { - return nil, err - } - - return stats, nil -} - -// GetRepoIssueStats returns number of open and closed repository issues by given filter mode. -func GetRepoIssueStats(repoID, uid int64, filterMode int, isPull bool) (numOpen, numClosed int64) { - countSession := func(isClosed, isPull bool, repoID int64) *xorm.Session { - sess := db.GetEngine(db.DefaultContext). - Where("is_closed = ?", isClosed). - And("is_pull = ?", isPull). - And("repo_id = ?", repoID) - - return sess - } - - openCountSession := countSession(false, isPull, repoID) - closedCountSession := countSession(true, isPull, repoID) - - switch filterMode { - case FilterModeAssign: - applyAssigneeCondition(openCountSession, uid) - applyAssigneeCondition(closedCountSession, uid) - case FilterModeCreate: - applyPosterCondition(openCountSession, uid) - applyPosterCondition(closedCountSession, uid) - } - - openResult, _ := openCountSession.Count(new(Issue)) - closedResult, _ := closedCountSession.Count(new(Issue)) - - return openResult, closedResult -} - -// SearchIssueIDsByKeyword search issues on database -func SearchIssueIDsByKeyword(ctx context.Context, kw string, repoIDs []int64, limit, start int) (int64, []int64, error) { - repoCond := builder.In("repo_id", repoIDs) - subQuery := builder.Select("id").From("issue").Where(repoCond) - // SQLite's UPPER function only transforms ASCII letters. - if setting.Database.UseSQLite3 { - kw = util.ToUpperASCII(kw) - } else { - kw = strings.ToUpper(kw) - } - cond := builder.And( - repoCond, - builder.Or( - builder.Like{"UPPER(name)", kw}, - builder.Like{"UPPER(content)", kw}, - builder.In("id", builder.Select("issue_id"). - From("comment"). - Where(builder.And( - builder.Eq{"type": CommentTypeComment}, - builder.In("issue_id", subQuery), - builder.Like{"UPPER(content)", kw}, - )), - ), - ), - ) - - ids := make([]int64, 0, limit) - res := make([]struct { - ID int64 - UpdatedUnix int64 - }, 0, limit) - err := db.GetEngine(ctx).Distinct("id", "updated_unix").Table("issue").Where(cond). - OrderBy("`updated_unix` DESC").Limit(limit, start). - Find(&res) - if err != nil { - return 0, nil, err - } - for _, r := range res { - ids = append(ids, r.ID) - } - - total, err := db.GetEngine(ctx).Distinct("id").Table("issue").Where(cond).Count() - if err != nil { - return 0, nil, err - } - - return total, ids, nil -} - -// UpdateIssueByAPI updates all allowed fields of given issue. -// If the issue status is changed a statusChangeComment is returned -// similarly if the title is changed the titleChanged bool is set to true -func UpdateIssueByAPI(issue *Issue, doer *user_model.User) (statusChangeComment *Comment, titleChanged bool, err error) { - ctx, committer, err := db.TxContext() - if err != nil { - return nil, false, err - } - defer committer.Close() - - if err := issue.LoadRepo(ctx); err != nil { - return nil, false, fmt.Errorf("loadRepo: %v", err) - } - - // Reload the issue - currentIssue, err := getIssueByID(ctx, issue.ID) - if err != nil { - return nil, false, err - } - - if _, err := db.GetEngine(ctx).ID(issue.ID).Cols( - "name", "content", "milestone_id", "priority", - "deadline_unix", "updated_unix", "is_locked"). - Update(issue); err != nil { - return nil, false, err - } - - titleChanged = currentIssue.Title != issue.Title - if titleChanged { - opts := &CreateCommentOptions{ - Type: CommentTypeChangeTitle, - Doer: doer, - Repo: issue.Repo, - Issue: issue, - OldTitle: currentIssue.Title, - NewTitle: issue.Title, - } - _, err := CreateCommentCtx(ctx, opts) - if err != nil { - return nil, false, fmt.Errorf("createComment: %v", err) - } - } - - if currentIssue.IsClosed != issue.IsClosed { - statusChangeComment, err = doChangeIssueStatus(ctx, issue, doer, false) - if err != nil { - return nil, false, err - } - } - - if err := issue.addCrossReferences(ctx, doer, true); err != nil { - return nil, false, err - } - return statusChangeComment, titleChanged, committer.Commit() -} - -// UpdateIssueDeadline updates an issue deadline and adds comments. Setting a deadline to 0 means deleting it. -func UpdateIssueDeadline(issue *Issue, deadlineUnix timeutil.TimeStamp, doer *user_model.User) (err error) { - // if the deadline hasn't changed do nothing - if issue.DeadlineUnix == deadlineUnix { - return nil - } - ctx, committer, err := db.TxContext() - if err != nil { - return err - } - defer committer.Close() - - // Update the deadline - if err = UpdateIssueCols(ctx, &Issue{ID: issue.ID, DeadlineUnix: deadlineUnix}, "deadline_unix"); err != nil { - return err - } - - // Make the comment - if _, err = createDeadlineComment(ctx, doer, issue, deadlineUnix); err != nil { - return fmt.Errorf("createRemovedDueDateComment: %v", err) - } - - 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 { - e := db.GetEngine(ctx) - for _, bean := range beans { - if _, err := e.In("issue_id", issueID).Delete(bean); err != nil { - return 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) { - if issue == nil { - return nil, nil - } - userIDs := make([]int64, 0, 5) - if err := db.GetEngine(ctx).Table("comment").Cols("poster_id"). - Where("`comment`.issue_id = ?", issue.ID). - And("`comment`.type in (?,?,?)", CommentTypeComment, CommentTypeCode, CommentTypeReview). - And("`user`.is_active = ?", true). - And("`user`.prohibit_login = ?", false). - Join("INNER", "`user`", "`user`.id = `comment`.poster_id"). - Distinct("poster_id"). - Find(&userIDs); err != nil { - return nil, fmt.Errorf("get poster IDs: %v", err) - } - if !util.IsInt64InSlice(issue.PosterID, userIDs) { - return append(userIDs, issue.PosterID), nil - } - return userIDs, nil -} - -// BlockedByDependencies finds all Dependencies an issue is blocked by -func (issue *Issue) BlockedByDependencies(ctx context.Context) (issueDeps []*DependencyInfo, err error) { - err = db.GetEngine(ctx). - Table("issue"). - Join("INNER", "repository", "repository.id = issue.repo_id"). - Join("INNER", "issue_dependency", "issue_dependency.dependency_id = issue.id"). - Where("issue_id = ?", issue.ID). - // sort by repo id then created date, with the issues of the same repo at the beginning of the list - OrderBy("CASE WHEN issue.repo_id = ? THEN 0 ELSE issue.repo_id END, issue.created_unix DESC", issue.RepoID). - Find(&issueDeps) - - for _, depInfo := range issueDeps { - depInfo.Issue.Repo = &depInfo.Repository - } - - return issueDeps, err -} - -// BlockingDependencies returns all blocking dependencies, aka all other issues a given issue blocks -func (issue *Issue) BlockingDependencies(ctx context.Context) (issueDeps []*DependencyInfo, err error) { - err = db.GetEngine(ctx). - Table("issue"). - Join("INNER", "repository", "repository.id = issue.repo_id"). - Join("INNER", "issue_dependency", "issue_dependency.issue_id = issue.id"). - Where("dependency_id = ?", issue.ID). - // sort by repo id then created date, with the issues of the same repo at the beginning of the list - OrderBy("CASE WHEN issue.repo_id = ? THEN 0 ELSE issue.repo_id END, issue.created_unix DESC", issue.RepoID). - Find(&issueDeps) - - for _, depInfo := range issueDeps { - depInfo.Issue.Repo = &depInfo.Repository - } - - return issueDeps, err -} - -func updateIssueClosedNum(ctx context.Context, issue *Issue) (err error) { - if issue.IsPull { - err = repoStatsCorrectNumClosed(ctx, issue.RepoID, true, "num_closed_pulls") - } else { - err = repoStatsCorrectNumClosed(ctx, issue.RepoID, false, "num_closed_issues") - } - return -} - -// FindAndUpdateIssueMentions finds users mentioned in the given content string, and saves them in the database. -func FindAndUpdateIssueMentions(ctx context.Context, issue *Issue, doer *user_model.User, content string) (mentions []*user_model.User, err error) { - rawMentions := references.FindAllMentionsMarkdown(content) - mentions, err = ResolveIssueMentionsByVisibility(ctx, issue, doer, rawMentions) - if err != nil { - return nil, fmt.Errorf("UpdateIssueMentions [%d]: %v", issue.ID, err) - } - if err = UpdateIssueMentions(ctx, issue.ID, mentions); err != nil { - return nil, fmt.Errorf("UpdateIssueMentions [%d]: %v", issue.ID, err) - } - return -} - -// ResolveIssueMentionsByVisibility returns the users mentioned in an issue, removing those that -// don't have access to reading it. Teams are expanded into their users, but organizations are ignored. -func ResolveIssueMentionsByVisibility(ctx context.Context, issue *Issue, doer *user_model.User, mentions []string) (users []*user_model.User, err error) { - if len(mentions) == 0 { - return - } - if err = issue.LoadRepo(ctx); err != nil { - return - } - - resolved := make(map[string]bool, 10) - var mentionTeams []string - - if err := issue.Repo.GetOwner(ctx); err != nil { - return nil, err - } - - repoOwnerIsOrg := issue.Repo.Owner.IsOrganization() - if repoOwnerIsOrg { - mentionTeams = make([]string, 0, 5) - } - - resolved[doer.LowerName] = true - for _, name := range mentions { - name := strings.ToLower(name) - if _, ok := resolved[name]; ok { - continue - } - if repoOwnerIsOrg && strings.Contains(name, "/") { - names := strings.Split(name, "/") - if len(names) < 2 || names[0] != issue.Repo.Owner.LowerName { - continue - } - mentionTeams = append(mentionTeams, names[1]) - resolved[name] = true - } else { - resolved[name] = false - } - } - - if issue.Repo.Owner.IsOrganization() && len(mentionTeams) > 0 { - teams := make([]*organization.Team, 0, len(mentionTeams)) - if err := db.GetEngine(ctx). - Join("INNER", "team_repo", "team_repo.team_id = team.id"). - Where("team_repo.repo_id=?", issue.Repo.ID). - In("team.lower_name", mentionTeams). - Find(&teams); err != nil { - return nil, fmt.Errorf("find mentioned teams: %v", err) - } - if len(teams) != 0 { - checked := make([]int64, 0, len(teams)) - unittype := unit.TypeIssues - if issue.IsPull { - unittype = unit.TypePullRequests - } - for _, team := range teams { - if team.AccessMode >= perm.AccessModeAdmin { - checked = append(checked, team.ID) - resolved[issue.Repo.Owner.LowerName+"/"+team.LowerName] = true - continue - } - has, err := db.GetEngine(ctx).Get(&organization.TeamUnit{OrgID: issue.Repo.Owner.ID, TeamID: team.ID, Type: unittype}) - if err != nil { - return nil, fmt.Errorf("get team units (%d): %v", team.ID, err) - } - if has { - checked = append(checked, team.ID) - resolved[issue.Repo.Owner.LowerName+"/"+team.LowerName] = true - } - } - if len(checked) != 0 { - teamusers := make([]*user_model.User, 0, 20) - if err := db.GetEngine(ctx). - Join("INNER", "team_user", "team_user.uid = `user`.id"). - In("`team_user`.team_id", checked). - And("`user`.is_active = ?", true). - And("`user`.prohibit_login = ?", false). - Find(&teamusers); err != nil { - return nil, fmt.Errorf("get teams users: %v", err) - } - if len(teamusers) > 0 { - users = make([]*user_model.User, 0, len(teamusers)) - for _, user := range teamusers { - if already, ok := resolved[user.LowerName]; !ok || !already { - users = append(users, user) - resolved[user.LowerName] = true - } - } - } - } - } - } - - // Remove names already in the list to avoid querying the database if pending names remain - mentionUsers := make([]string, 0, len(resolved)) - for name, already := range resolved { - if !already { - mentionUsers = append(mentionUsers, name) - } - } - if len(mentionUsers) == 0 { - return - } - - if users == nil { - users = make([]*user_model.User, 0, len(mentionUsers)) - } - - unchecked := make([]*user_model.User, 0, len(mentionUsers)) - if err := db.GetEngine(ctx). - Where("`user`.is_active = ?", true). - And("`user`.prohibit_login = ?", false). - In("`user`.lower_name", mentionUsers). - Find(&unchecked); err != nil { - return nil, fmt.Errorf("find mentioned users: %v", err) - } - for _, user := range unchecked { - if already := resolved[user.LowerName]; already || user.IsOrganization() { - continue - } - // Normal users must have read access to the referencing issue - perm, err := access_model.GetUserRepoPermission(ctx, issue.Repo, user) - if err != nil { - return nil, fmt.Errorf("GetUserRepoPermission [%d]: %v", user.ID, err) - } - if !perm.CanReadIssuesOrPulls(issue.IsPull) { - continue - } - users = append(users, user) - } - - return -} - -// UpdateIssuesMigrationsByType updates all migrated repositories' issues from gitServiceType to replace originalAuthorID to posterID -func UpdateIssuesMigrationsByType(gitServiceType api.GitServiceType, originalAuthorID string, posterID int64) error { - _, err := db.GetEngine(db.DefaultContext).Table("issue"). - Where("repo_id IN (SELECT id FROM repository WHERE original_service_type = ?)", gitServiceType). - And("original_author_id = ?", originalAuthorID). - Update(map[string]interface{}{ - "poster_id": posterID, - "original_author": "", - "original_author_id": 0, - }) - return err -} - -// 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"). - Where("original_author_id = ?", originalAuthorID). - And(migratedIssueCond(gitServiceType)). - Update(map[string]interface{}{ - "user_id": userID, - "original_author": "", - "original_author_id": 0, - }) - return err -} - -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 { - return - } - - // Delete comments and attachments - if _, err = sess.In("issue_id", deleteCond). - Delete(&Comment{}); err != nil { - return - } - - // Dependencies for issues in this repository - if _, err = sess.In("issue_id", deleteCond). - Delete(&IssueDependency{}); err != nil { - return - } - - // Delete dependencies for issues in other repositories - if _, err = sess.In("dependency_id", deleteCond). - Delete(&IssueDependency{}); err != nil { - return - } - - if _, err = sess.In("issue_id", deleteCond). - Delete(&IssueUser{}); err != nil { - return - } - - if _, err = sess.In("issue_id", deleteCond). - Delete(&issues_model.Reaction{}); err != nil { - return - } - - if _, err = sess.In("issue_id", deleteCond). - Delete(&IssueWatch{}); err != nil { - return - } - - if _, err = sess.In("issue_id", deleteCond). - Delete(&Stopwatch{}); err != nil { - return - } - - if _, err = sess.In("issue_id", deleteCond). - Delete(&TrackedTime{}); err != nil { - return - } - - if _, err = sess.In("issue_id", deleteCond). - Delete(&project_model.ProjectIssue{}); err != nil { - return - } - - if _, err = sess.In("dependent_issue_id", deleteCond). - Delete(&Comment{}); err != nil { - return - } - - var attachments []*repo_model.Attachment - if err = sess.In("issue_id", deleteCond). - Find(&attachments); err != nil { - return - } - - for j := range attachments { - attachmentPaths = append(attachmentPaths, attachments[j].RelativePath()) - } - - if _, err = sess.In("issue_id", deleteCond). - Delete(&repo_model.Attachment{}); err != nil { - return - } - - if _, err = db.DeleteByBean(ctx, &Issue{RepoID: repoID}); err != nil { - return - } - - return -} - -// RemapExternalUser ExternalUserRemappable interface -func (issue *Issue) RemapExternalUser(externalName string, externalID, userID int64) error { - issue.OriginalAuthor = externalName - issue.OriginalAuthorID = externalID - issue.PosterID = userID - return nil -} - -// GetUserID ExternalUserRemappable interface -func (issue *Issue) GetUserID() int64 { return issue.PosterID } - -// GetExternalName ExternalUserRemappable interface -func (issue *Issue) GetExternalName() string { return issue.OriginalAuthor } - -// GetExternalID ExternalUserRemappable interface -func (issue *Issue) GetExternalID() int64 { return issue.OriginalAuthorID } diff --git a/models/issue_assignees.go b/models/issue_assignees.go deleted file mode 100644 index c6ccb6e9d2..0000000000 --- a/models/issue_assignees.go +++ /dev/null @@ -1,171 +0,0 @@ -// Copyright 2018 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" - "fmt" - - "code.gitea.io/gitea/models/db" - user_model "code.gitea.io/gitea/models/user" - "code.gitea.io/gitea/modules/util" -) - -// IssueAssignees saves all issue assignees -type IssueAssignees struct { - ID int64 `xorm:"pk autoincr"` - AssigneeID int64 `xorm:"INDEX"` - IssueID int64 `xorm:"INDEX"` -} - -func init() { - db.RegisterModel(new(IssueAssignees)) -} - -// LoadAssignees load assignees of this issue. -func (issue *Issue) LoadAssignees(ctx context.Context) (err error) { - // Reset maybe preexisting assignees - issue.Assignees = []*user_model.User{} - issue.Assignee = nil - - err = db.GetEngine(ctx).Table("`user`"). - Join("INNER", "issue_assignees", "assignee_id = `user`.id"). - Where("issue_assignees.issue_id = ?", issue.ID). - Find(&issue.Assignees) - if err != nil { - return err - } - - // Check if we have at least one assignee and if yes put it in as `Assignee` - if len(issue.Assignees) > 0 { - issue.Assignee = issue.Assignees[0] - } - return -} - -// GetAssigneeIDsByIssue returns the IDs of users assigned to an issue -// but skips joining with `user` for performance reasons. -// User permissions must be verified elsewhere if required. -func GetAssigneeIDsByIssue(issueID int64) ([]int64, error) { - userIDs := make([]int64, 0, 5) - return userIDs, db.GetEngine(db.DefaultContext).Table("issue_assignees"). - Cols("assignee_id"). - Where("issue_id = ?", issueID). - Distinct("assignee_id"). - Find(&userIDs) -} - -// IsUserAssignedToIssue returns true when the user is assigned to the issue -func IsUserAssignedToIssue(ctx context.Context, issue *Issue, user *user_model.User) (isAssigned bool, err error) { - return db.GetByBean(ctx, &IssueAssignees{IssueID: issue.ID, AssigneeID: user.ID}) -} - -// ToggleIssueAssignee changes a user between assigned and not assigned for this issue, and make issue comment for it. -func ToggleIssueAssignee(issue *Issue, doer *user_model.User, assigneeID int64) (removed bool, comment *Comment, err error) { - ctx, committer, err := db.TxContext() - if err != nil { - return false, nil, err - } - defer committer.Close() - - removed, comment, err = toggleIssueAssignee(ctx, issue, doer, assigneeID, false) - if err != nil { - return false, nil, err - } - - if err := committer.Commit(); err != nil { - return false, nil, err - } - - return removed, comment, nil -} - -func toggleIssueAssignee(ctx context.Context, issue *Issue, doer *user_model.User, assigneeID int64, isCreate bool) (removed bool, comment *Comment, err error) { - removed, err = toggleUserAssignee(ctx, issue, assigneeID) - if err != nil { - return false, nil, fmt.Errorf("UpdateIssueUserByAssignee: %v", err) - } - - // Repo infos - if err = issue.LoadRepo(ctx); err != nil { - return false, nil, fmt.Errorf("loadRepo: %v", err) - } - - opts := &CreateCommentOptions{ - Type: CommentTypeAssignees, - Doer: doer, - Repo: issue.Repo, - Issue: issue, - RemovedAssignee: removed, - AssigneeID: assigneeID, - } - // Comment - comment, err = CreateCommentCtx(ctx, opts) - if err != nil { - return false, nil, fmt.Errorf("createComment: %v", err) - } - - // if pull request is in the middle of creation - don't call webhook - if isCreate { - return removed, comment, err - } - - return removed, comment, nil -} - -// toggles user assignee state in database -func toggleUserAssignee(ctx context.Context, issue *Issue, assigneeID int64) (removed bool, err error) { - // Check if the user exists - assignee, err := user_model.GetUserByIDCtx(ctx, assigneeID) - if err != nil { - return false, err - } - - // Check if the submitted user is already assigned, if yes delete him otherwise add him - found := false - i := 0 - for ; i < len(issue.Assignees); i++ { - if issue.Assignees[i].ID == assigneeID { - found = true - break - } - } - - assigneeIn := IssueAssignees{AssigneeID: assigneeID, IssueID: issue.ID} - if found { - issue.Assignees = append(issue.Assignees[:i], issue.Assignees[i+1:]...) - _, err = db.DeleteByBean(ctx, &assigneeIn) - if err != nil { - return found, err - } - } else { - issue.Assignees = append(issue.Assignees, assignee) - if err = db.Insert(ctx, &assigneeIn); err != nil { - return found, err - } - } - - return found, nil -} - -// MakeIDsFromAPIAssigneesToAdd returns an array with all assignee IDs -func MakeIDsFromAPIAssigneesToAdd(oneAssignee string, multipleAssignees []string) (assigneeIDs []int64, err error) { - var requestAssignees []string - - // Keeping the old assigning method for compatibility reasons - if oneAssignee != "" && !util.IsStringInSlice(oneAssignee, multipleAssignees) { - requestAssignees = append(requestAssignees, oneAssignee) - } - - // Prevent empty assignees - if len(multipleAssignees) > 0 && multipleAssignees[0] != "" { - requestAssignees = append(requestAssignees, multipleAssignees...) - } - - // Get the IDs of all assignees - assigneeIDs, err = user_model.GetUserIDsByNames(requestAssignees, false) - - return -} diff --git a/models/issue_assignees_test.go b/models/issue_assignees_test.go deleted file mode 100644 index 80317e1604..0000000000 --- a/models/issue_assignees_test.go +++ /dev/null @@ -1,91 +0,0 @@ -// Copyright 2018 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" - - "github.com/stretchr/testify/assert" -) - -func TestUpdateAssignee(t *testing.T) { - assert.NoError(t, unittest.PrepareTestDatabase()) - - // Fake issue with assignees - issue, err := 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) - assert.NoError(t, err) - - user3, err := user_model.GetUserByID(3) - assert.NoError(t, err) - _, _, err = 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) - assert.NoError(t, err) - - // Check if he got removed - isAssigned, err := IsUserAssignedToIssue(db.DefaultContext, issue, user1) - assert.NoError(t, err) - assert.False(t, isAssigned) - - // Check if they're all there - err = issue.LoadAssignees(db.DefaultContext) - assert.NoError(t, err) - - var expectedAssignees []*user_model.User - expectedAssignees = append(expectedAssignees, user2, user3) - - for in, assignee := range issue.Assignees { - assert.Equal(t, assignee.ID, expectedAssignees[in].ID) - } - - // Check if the user is assigned - isAssigned, err = 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}) - assert.NoError(t, err) - assert.False(t, isAssigned) -} - -func TestMakeIDsFromAPIAssigneesToAdd(t *testing.T) { - assert.NoError(t, unittest.PrepareTestDatabase()) - - _ = 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{""}) - assert.NoError(t, err) - assert.Equal(t, []int64{}, IDs) - - _, err = MakeIDsFromAPIAssigneesToAdd("", []string{"none_existing_user"}) - assert.Error(t, err) - - IDs, err = MakeIDsFromAPIAssigneesToAdd("user1", []string{"user1"}) - assert.NoError(t, err) - assert.Equal(t, []int64{1}, IDs) - - IDs, err = MakeIDsFromAPIAssigneesToAdd("user2", []string{""}) - assert.NoError(t, err) - assert.Equal(t, []int64{2}, IDs) - - IDs, err = MakeIDsFromAPIAssigneesToAdd("", []string{"user1", "user2"}) - assert.NoError(t, err) - assert.Equal(t, []int64{1, 2}, IDs) -} diff --git a/models/issue_comment.go b/models/issue_comment.go deleted file mode 100644 index 21cd87108d..0000000000 --- a/models/issue_comment.go +++ /dev/null @@ -1,1502 +0,0 @@ -// Copyright 2018 The Gitea Authors. -// Copyright 2016 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 ( - "context" - "fmt" - "regexp" - "strconv" - "strings" - "unicode/utf8" - - "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" - user_model "code.gitea.io/gitea/models/user" - "code.gitea.io/gitea/modules/git" - "code.gitea.io/gitea/modules/json" - "code.gitea.io/gitea/modules/log" - "code.gitea.io/gitea/modules/markup" - "code.gitea.io/gitea/modules/markup/markdown" - "code.gitea.io/gitea/modules/references" - "code.gitea.io/gitea/modules/structs" - "code.gitea.io/gitea/modules/timeutil" - - "xorm.io/builder" - "xorm.io/xorm" -) - -// CommentType defines whether a comment is just a simple comment, an action (like close) or a reference. -type CommentType int - -// define unknown comment type -const ( - CommentTypeUnknown CommentType = -1 -) - -// Enumerate all the comment types -const ( - // 0 Plain comment, can be associated with a commit (CommitID > 0) and a line (LineNum > 0) - CommentTypeComment CommentType = iota - CommentTypeReopen // 1 - CommentTypeClose // 2 - - // 3 References. - CommentTypeIssueRef - // 4 Reference from a commit (not part of a pull request) - CommentTypeCommitRef - // 5 Reference from a comment - CommentTypeCommentRef - // 6 Reference from a pull request - CommentTypePullRef - // 7 Labels changed - CommentTypeLabel - // 8 Milestone changed - CommentTypeMilestone - // 9 Assignees changed - CommentTypeAssignees - // 10 Change Title - CommentTypeChangeTitle - // 11 Delete Branch - CommentTypeDeleteBranch - // 12 Start a stopwatch for time tracking - CommentTypeStartTracking - // 13 Stop a stopwatch for time tracking - CommentTypeStopTracking - // 14 Add time manual for time tracking - CommentTypeAddTimeManual - // 15 Cancel a stopwatch for time tracking - CommentTypeCancelTracking - // 16 Added a due date - CommentTypeAddedDeadline - // 17 Modified the due date - CommentTypeModifiedDeadline - // 18 Removed a due date - CommentTypeRemovedDeadline - // 19 Dependency added - CommentTypeAddDependency - // 20 Dependency removed - CommentTypeRemoveDependency - // 21 Comment a line of code - CommentTypeCode - // 22 Reviews a pull request by giving general feedback - CommentTypeReview - // 23 Lock an issue, giving only collaborators access - CommentTypeLock - // 24 Unlocks a previously locked issue - CommentTypeUnlock - // 25 Change pull request's target branch - CommentTypeChangeTargetBranch - // 26 Delete time manual for time tracking - CommentTypeDeleteTimeManual - // 27 add or remove Request from one - CommentTypeReviewRequest - // 28 merge pull request - CommentTypeMergePull - // 29 push to PR head branch - CommentTypePullRequestPush - // 30 Project changed - CommentTypeProject - // 31 Project board changed - CommentTypeProjectBoard - // 32 Dismiss Review - CommentTypeDismissReview - // 33 Change issue ref - CommentTypeChangeIssueRef - // 34 pr was scheduled to auto merge when checks succeed - CommentTypePRScheduledToAutoMerge - // 35 pr was un scheduled to auto merge when checks succeed - CommentTypePRUnScheduledToAutoMerge -) - -var commentStrings = []string{ - "comment", - "reopen", - "close", - "issue_ref", - "commit_ref", - "comment_ref", - "pull_ref", - "label", - "milestone", - "assignees", - "change_title", - "delete_branch", - "start_tracking", - "stop_tracking", - "add_time_manual", - "cancel_tracking", - "added_deadline", - "modified_deadline", - "removed_deadline", - "add_dependency", - "remove_dependency", - "code", - "review", - "lock", - "unlock", - "change_target_branch", - "delete_time_manual", - "review_request", - "merge_pull", - "pull_push", - "project", - "project_board", - "dismiss_review", - "change_issue_ref", - "pull_scheduled_merge", - "pull_cancel_scheduled_merge", -} - -func (t CommentType) String() string { - return commentStrings[t] -} - -// RoleDescriptor defines comment tag type -type RoleDescriptor int - -// Enumerate all the role tags. -const ( - RoleDescriptorNone RoleDescriptor = iota - RoleDescriptorPoster - RoleDescriptorWriter - RoleDescriptorOwner -) - -// WithRole enable a specific tag on the RoleDescriptor. -func (rd RoleDescriptor) WithRole(role RoleDescriptor) RoleDescriptor { - return rd | (1 << role) -} - -func stringToRoleDescriptor(role string) RoleDescriptor { - switch role { - case "Poster": - return RoleDescriptorPoster - case "Writer": - return RoleDescriptorWriter - case "Owner": - return RoleDescriptorOwner - default: - return RoleDescriptorNone - } -} - -// HasRole returns if a certain role is enabled on the RoleDescriptor. -func (rd RoleDescriptor) HasRole(role string) bool { - roleDescriptor := stringToRoleDescriptor(role) - bitValue := rd & (1 << roleDescriptor) - return (bitValue > 0) -} - -// Comment represents a comment in commit and issue page. -type Comment struct { - ID int64 `xorm:"pk autoincr"` - Type CommentType `xorm:"INDEX"` - PosterID int64 `xorm:"INDEX"` - Poster *user_model.User `xorm:"-"` - OriginalAuthor string - OriginalAuthorID int64 - IssueID int64 `xorm:"INDEX"` - Issue *Issue `xorm:"-"` - LabelID int64 - Label *Label `xorm:"-"` - AddedLabels []*Label `xorm:"-"` - RemovedLabels []*Label `xorm:"-"` - OldProjectID int64 - ProjectID int64 - OldProject *project_model.Project `xorm:"-"` - Project *project_model.Project `xorm:"-"` - OldMilestoneID int64 - MilestoneID int64 - OldMilestone *issues_model.Milestone `xorm:"-"` - Milestone *issues_model.Milestone `xorm:"-"` - TimeID int64 - Time *TrackedTime `xorm:"-"` - AssigneeID int64 - RemovedAssignee bool - Assignee *user_model.User `xorm:"-"` - AssigneeTeamID int64 `xorm:"NOT NULL DEFAULT 0"` - AssigneeTeam *organization.Team `xorm:"-"` - ResolveDoerID int64 - ResolveDoer *user_model.User `xorm:"-"` - OldTitle string - NewTitle string - OldRef string - NewRef string - DependentIssueID int64 - DependentIssue *Issue `xorm:"-"` - - CommitID int64 - Line int64 // - previous line / + proposed line - TreePath string - Content string `xorm:"LONGTEXT"` - RenderedContent string `xorm:"-"` - - // Path represents the 4 lines of code cemented by this comment - Patch string `xorm:"-"` - PatchQuoted string `xorm:"LONGTEXT patch"` - - CreatedUnix timeutil.TimeStamp `xorm:"INDEX created"` - UpdatedUnix timeutil.TimeStamp `xorm:"INDEX updated"` - - // Reference issue in commit message - CommitSHA string `xorm:"VARCHAR(40)"` - - Attachments []*repo_model.Attachment `xorm:"-"` - Reactions issues_model.ReactionList `xorm:"-"` - - // For view issue page. - ShowRole RoleDescriptor `xorm:"-"` - - Review *Review `xorm:"-"` - ReviewID int64 `xorm:"index"` - Invalidated bool - - // Reference an issue or pull from another comment, issue or PR - // All information is about the origin of the reference - RefRepoID int64 `xorm:"index"` // Repo where the referencing - RefIssueID int64 `xorm:"index"` - RefCommentID int64 `xorm:"index"` // 0 if origin is Issue title or content (or PR's) - RefAction references.XRefAction `xorm:"SMALLINT"` // What happens if RefIssueID resolves - RefIsPull bool - - RefRepo *repo_model.Repository `xorm:"-"` - RefIssue *Issue `xorm:"-"` - RefComment *Comment `xorm:"-"` - - Commits []*git_model.SignCommitWithStatuses `xorm:"-"` - OldCommit string `xorm:"-"` - NewCommit string `xorm:"-"` - CommitsNum int64 `xorm:"-"` - IsForcePush bool `xorm:"-"` -} - -func init() { - db.RegisterModel(new(Comment)) -} - -// PushActionContent is content of push pull comment -type PushActionContent struct { - IsForcePush bool `json:"is_force_push"` - CommitIDs []string `json:"commit_ids"` -} - -// LoadIssue loads issue from database -func (c *Comment) LoadIssue() (err error) { - return c.LoadIssueCtx(db.DefaultContext) -} - -// LoadIssueCtx loads issue from database -func (c *Comment) LoadIssueCtx(ctx context.Context) (err error) { - if c.Issue != nil { - return nil - } - c.Issue, err = getIssueByID(ctx, c.IssueID) - return -} - -// BeforeInsert will be invoked by XORM before inserting a record -func (c *Comment) BeforeInsert() { - c.PatchQuoted = c.Patch - if !utf8.ValidString(c.Patch) { - c.PatchQuoted = strconv.Quote(c.Patch) - } -} - -// BeforeUpdate will be invoked by XORM before updating a record -func (c *Comment) BeforeUpdate() { - c.PatchQuoted = c.Patch - if !utf8.ValidString(c.Patch) { - c.PatchQuoted = strconv.Quote(c.Patch) - } -} - -// AfterLoad is invoked from XORM after setting the values of all fields of this object. -func (c *Comment) AfterLoad(session *xorm.Session) { - c.Patch = c.PatchQuoted - if len(c.PatchQuoted) > 0 && c.PatchQuoted[0] == '"' { - unquoted, err := strconv.Unquote(c.PatchQuoted) - if err == nil { - c.Patch = unquoted - } - } -} - -func (c *Comment) loadPoster(ctx context.Context) (err error) { - if c.PosterID <= 0 || c.Poster != nil { - return nil - } - - c.Poster, err = user_model.GetUserByIDCtx(ctx, c.PosterID) - if err != nil { - if user_model.IsErrUserNotExist(err) { - c.PosterID = -1 - c.Poster = user_model.NewGhostUser() - } else { - log.Error("getUserByID[%d]: %v", c.ID, err) - } - } - return err -} - -// AfterDelete is invoked from XORM after the object is deleted. -func (c *Comment) AfterDelete() { - if c.ID <= 0 { - return - } - - _, err := repo_model.DeleteAttachmentsByComment(c.ID, true) - if err != nil { - log.Info("Could not delete files for comment %d on issue #%d: %s", c.ID, c.IssueID, err) - } -} - -// HTMLURL formats a URL-string to the issue-comment -func (c *Comment) HTMLURL() string { - err := c.LoadIssue() - if err != nil { // Silently dropping errors :unamused: - log.Error("LoadIssue(%d): %v", c.IssueID, err) - return "" - } - err = c.Issue.LoadRepo(db.DefaultContext) - if err != nil { // Silently dropping errors :unamused: - log.Error("loadRepo(%d): %v", c.Issue.RepoID, err) - return "" - } - if c.Type == CommentTypeCode { - if c.ReviewID == 0 { - return fmt.Sprintf("%s/files#%s", c.Issue.HTMLURL(), c.HashTag()) - } - if c.Review == nil { - if err := c.LoadReview(); err != nil { - log.Warn("LoadReview(%d): %v", c.ReviewID, err) - return fmt.Sprintf("%s/files#%s", c.Issue.HTMLURL(), c.HashTag()) - } - } - if c.Review.Type <= ReviewTypePending { - return fmt.Sprintf("%s/files#%s", c.Issue.HTMLURL(), c.HashTag()) - } - } - return fmt.Sprintf("%s#%s", c.Issue.HTMLURL(), c.HashTag()) -} - -// APIURL formats a API-string to the issue-comment -func (c *Comment) APIURL() string { - err := c.LoadIssue() - if err != nil { // Silently dropping errors :unamused: - log.Error("LoadIssue(%d): %v", c.IssueID, err) - return "" - } - err = c.Issue.LoadRepo(db.DefaultContext) - if err != nil { // Silently dropping errors :unamused: - log.Error("loadRepo(%d): %v", c.Issue.RepoID, err) - return "" - } - - return fmt.Sprintf("%s/issues/comments/%d", c.Issue.Repo.APIURL(), c.ID) -} - -// IssueURL formats a URL-string to the issue -func (c *Comment) IssueURL() string { - err := c.LoadIssue() - if err != nil { // Silently dropping errors :unamused: - log.Error("LoadIssue(%d): %v", c.IssueID, err) - return "" - } - - if c.Issue.IsPull { - return "" - } - - err = c.Issue.LoadRepo(db.DefaultContext) - if err != nil { // Silently dropping errors :unamused: - log.Error("loadRepo(%d): %v", c.Issue.RepoID, err) - return "" - } - return c.Issue.HTMLURL() -} - -// PRURL formats a URL-string to the pull-request -func (c *Comment) PRURL() string { - err := c.LoadIssue() - if err != nil { // Silently dropping errors :unamused: - log.Error("LoadIssue(%d): %v", c.IssueID, err) - return "" - } - - err = c.Issue.LoadRepo(db.DefaultContext) - if err != nil { // Silently dropping errors :unamused: - log.Error("loadRepo(%d): %v", c.Issue.RepoID, err) - return "" - } - - if !c.Issue.IsPull { - return "" - } - return c.Issue.HTMLURL() -} - -// CommentHashTag returns unique hash tag for comment id. -func CommentHashTag(id int64) string { - return fmt.Sprintf("issuecomment-%d", id) -} - -// HashTag returns unique hash tag for comment. -func (c *Comment) HashTag() string { - return CommentHashTag(c.ID) -} - -// EventTag returns unique event hash tag for comment. -func (c *Comment) EventTag() string { - return fmt.Sprintf("event-%d", c.ID) -} - -// LoadLabel if comment.Type is CommentTypeLabel, then load Label -func (c *Comment) LoadLabel() error { - var label Label - has, err := db.GetEngine(db.DefaultContext).ID(c.LabelID).Get(&label) - if err != nil { - return err - } else if has { - c.Label = &label - } else { - // Ignore Label is deleted, but not clear this table - log.Warn("Commit %d cannot load label %d", c.ID, c.LabelID) - } - - return nil -} - -// LoadProject if comment.Type is CommentTypeProject, then load project. -func (c *Comment) LoadProject() error { - if c.OldProjectID > 0 { - var oldProject project_model.Project - has, err := db.GetEngine(db.DefaultContext).ID(c.OldProjectID).Get(&oldProject) - if err != nil { - return err - } else if has { - c.OldProject = &oldProject - } - } - - if c.ProjectID > 0 { - var project project_model.Project - has, err := db.GetEngine(db.DefaultContext).ID(c.ProjectID).Get(&project) - if err != nil { - return err - } else if has { - c.Project = &project - } - } - - return nil -} - -// LoadMilestone if comment.Type is CommentTypeMilestone, then load milestone -func (c *Comment) LoadMilestone() error { - if c.OldMilestoneID > 0 { - var oldMilestone issues_model.Milestone - has, err := db.GetEngine(db.DefaultContext).ID(c.OldMilestoneID).Get(&oldMilestone) - if err != nil { - return err - } else if has { - c.OldMilestone = &oldMilestone - } - } - - if c.MilestoneID > 0 { - var milestone issues_model.Milestone - has, err := db.GetEngine(db.DefaultContext).ID(c.MilestoneID).Get(&milestone) - if err != nil { - return err - } else if has { - c.Milestone = &milestone - } - } - return nil -} - -// LoadPoster loads comment poster -func (c *Comment) LoadPoster() error { - return c.loadPoster(db.DefaultContext) -} - -// LoadAttachments loads attachments (it never returns error, the error during `GetAttachmentsByCommentIDCtx` is ignored) -func (c *Comment) LoadAttachments() error { - if len(c.Attachments) > 0 { - return nil - } - - var err error - c.Attachments, err = repo_model.GetAttachmentsByCommentID(db.DefaultContext, c.ID) - if err != nil { - log.Error("getAttachmentsByCommentID[%d]: %v", c.ID, err) - } - return nil -} - -// UpdateAttachments update attachments by UUIDs for the comment -func (c *Comment) UpdateAttachments(uuids []string) error { - ctx, committer, err := db.TxContext() - if err != nil { - return err - } - defer committer.Close() - - attachments, err := repo_model.GetAttachmentsByUUIDs(ctx, uuids) - if err != nil { - return fmt.Errorf("getAttachmentsByUUIDs [uuids: %v]: %v", uuids, err) - } - for i := 0; i < len(attachments); i++ { - attachments[i].IssueID = c.IssueID - attachments[i].CommentID = c.ID - if err := repo_model.UpdateAttachment(ctx, attachments[i]); err != nil { - return fmt.Errorf("update attachment [id: %d]: %v", attachments[i].ID, err) - } - } - return committer.Commit() -} - -// LoadAssigneeUserAndTeam if comment.Type is CommentTypeAssignees, then load assignees -func (c *Comment) LoadAssigneeUserAndTeam() error { - var err error - - if c.AssigneeID > 0 && c.Assignee == nil { - c.Assignee, err = user_model.GetUserByIDCtx(db.DefaultContext, c.AssigneeID) - if err != nil { - if !user_model.IsErrUserNotExist(err) { - return err - } - c.Assignee = user_model.NewGhostUser() - } - } else if c.AssigneeTeamID > 0 && c.AssigneeTeam == nil { - if err = c.LoadIssue(); err != nil { - return err - } - - if err = c.Issue.LoadRepo(db.DefaultContext); err != nil { - return err - } - - if err = c.Issue.Repo.GetOwner(db.DefaultContext); err != nil { - return err - } - - if c.Issue.Repo.Owner.IsOrganization() { - c.AssigneeTeam, err = organization.GetTeamByID(db.DefaultContext, c.AssigneeTeamID) - if err != nil && !organization.IsErrTeamNotExist(err) { - return err - } - } - } - return nil -} - -// LoadResolveDoer if comment.Type is CommentTypeCode and ResolveDoerID not zero, then load resolveDoer -func (c *Comment) LoadResolveDoer() (err error) { - if c.ResolveDoerID == 0 || c.Type != CommentTypeCode { - return nil - } - c.ResolveDoer, err = user_model.GetUserByIDCtx(db.DefaultContext, c.ResolveDoerID) - if err != nil { - if user_model.IsErrUserNotExist(err) { - c.ResolveDoer = user_model.NewGhostUser() - err = nil - } - } - return -} - -// IsResolved check if an code comment is resolved -func (c *Comment) IsResolved() bool { - return c.ResolveDoerID != 0 && c.Type == CommentTypeCode -} - -// LoadDepIssueDetails loads Dependent Issue Details -func (c *Comment) LoadDepIssueDetails() (err error) { - if c.DependentIssueID <= 0 || c.DependentIssue != nil { - return nil - } - c.DependentIssue, err = getIssueByID(db.DefaultContext, c.DependentIssueID) - return err -} - -// LoadTime loads the associated time for a CommentTypeAddTimeManual -func (c *Comment) LoadTime() error { - if c.Time != nil || c.TimeID == 0 { - return nil - } - var err error - c.Time, err = GetTrackedTimeByID(c.TimeID) - return err -} - -func (c *Comment) loadReactions(ctx context.Context, repo *repo_model.Repository) (err error) { - if c.Reactions != nil { - return nil - } - c.Reactions, _, err = issues_model.FindReactions(ctx, issues_model.FindReactionsOptions{ - IssueID: c.IssueID, - CommentID: c.ID, - }) - if err != nil { - return err - } - // Load reaction user data - if _, err := c.Reactions.LoadUsers(ctx, repo); err != nil { - return err - } - return nil -} - -// LoadReactions loads comment reactions -func (c *Comment) LoadReactions(repo *repo_model.Repository) error { - return c.loadReactions(db.DefaultContext, repo) -} - -func (c *Comment) loadReview(ctx context.Context) (err error) { - if c.Review == nil { - if c.Review, err = GetReviewByID(ctx, c.ReviewID); err != nil { - return err - } - } - c.Review.Issue = c.Issue - return nil -} - -// LoadReview loads the associated review -func (c *Comment) LoadReview() error { - return c.loadReview(db.DefaultContext) -} - -var notEnoughLines = regexp.MustCompile(`fatal: file .* has only \d+ lines?`) - -func (c *Comment) checkInvalidation(doer *user_model.User, repo *git.Repository, branch string) error { - // FIXME differentiate between previous and proposed line - commit, err := repo.LineBlame(branch, repo.Path, c.TreePath, uint(c.UnsignedLine())) - if err != nil && (strings.Contains(err.Error(), "fatal: no such path") || notEnoughLines.MatchString(err.Error())) { - c.Invalidated = true - return UpdateComment(c, doer) - } - if err != nil { - return err - } - if c.CommitSHA != "" && c.CommitSHA != commit.ID.String() { - c.Invalidated = true - return UpdateComment(c, doer) - } - return nil -} - -// CheckInvalidation checks if the line of code comment got changed by another commit. -// If the line got changed the comment is going to be invalidated. -func (c *Comment) CheckInvalidation(repo *git.Repository, doer *user_model.User, branch string) error { - return c.checkInvalidation(doer, repo, branch) -} - -// DiffSide returns "previous" if Comment.Line is a LOC of the previous changes and "proposed" if it is a LOC of the proposed changes. -func (c *Comment) DiffSide() string { - if c.Line < 0 { - return "previous" - } - return "proposed" -} - -// UnsignedLine returns the LOC of the code comment without + or - -func (c *Comment) UnsignedLine() uint64 { - if c.Line < 0 { - return uint64(c.Line * -1) - } - return uint64(c.Line) -} - -// CodeCommentURL returns the url to a comment in code -func (c *Comment) CodeCommentURL() string { - err := c.LoadIssue() - if err != nil { // Silently dropping errors :unamused: - log.Error("LoadIssue(%d): %v", c.IssueID, err) - return "" - } - err = c.Issue.LoadRepo(db.DefaultContext) - if err != nil { // Silently dropping errors :unamused: - log.Error("loadRepo(%d): %v", c.Issue.RepoID, err) - return "" - } - return fmt.Sprintf("%s/files#%s", c.Issue.HTMLURL(), c.HashTag()) -} - -// LoadPushCommits Load push commits -func (c *Comment) LoadPushCommits(ctx context.Context) (err error) { - if c.Content == "" || c.Commits != nil || c.Type != CommentTypePullRequestPush { - return nil - } - - var data PushActionContent - - err = json.Unmarshal([]byte(c.Content), &data) - if err != nil { - return - } - - c.IsForcePush = data.IsForcePush - - if c.IsForcePush { - if len(data.CommitIDs) != 2 { - return nil - } - c.OldCommit = data.CommitIDs[0] - c.NewCommit = data.CommitIDs[1] - } else { - repoPath := c.Issue.Repo.RepoPath() - gitRepo, closer, err := git.RepositoryFromContextOrOpen(ctx, repoPath) - if err != nil { - return err - } - defer closer.Close() - - c.Commits = git_model.ConvertFromGitCommit(gitRepo.GetCommitsFromIDs(data.CommitIDs), c.Issue.Repo) - c.CommitsNum = int64(len(c.Commits)) - } - - return err -} - -// CreateCommentCtx creates comment with context -func CreateCommentCtx(ctx context.Context, opts *CreateCommentOptions) (_ *Comment, err error) { - e := db.GetEngine(ctx) - var LabelID int64 - if opts.Label != nil { - LabelID = opts.Label.ID - } - - comment := &Comment{ - Type: opts.Type, - PosterID: opts.Doer.ID, - Poster: opts.Doer, - IssueID: opts.Issue.ID, - LabelID: LabelID, - OldMilestoneID: opts.OldMilestoneID, - MilestoneID: opts.MilestoneID, - OldProjectID: opts.OldProjectID, - ProjectID: opts.ProjectID, - TimeID: opts.TimeID, - RemovedAssignee: opts.RemovedAssignee, - AssigneeID: opts.AssigneeID, - AssigneeTeamID: opts.AssigneeTeamID, - CommitID: opts.CommitID, - CommitSHA: opts.CommitSHA, - Line: opts.LineNum, - Content: opts.Content, - OldTitle: opts.OldTitle, - NewTitle: opts.NewTitle, - OldRef: opts.OldRef, - NewRef: opts.NewRef, - DependentIssueID: opts.DependentIssueID, - TreePath: opts.TreePath, - ReviewID: opts.ReviewID, - Patch: opts.Patch, - RefRepoID: opts.RefRepoID, - RefIssueID: opts.RefIssueID, - RefCommentID: opts.RefCommentID, - RefAction: opts.RefAction, - RefIsPull: opts.RefIsPull, - IsForcePush: opts.IsForcePush, - Invalidated: opts.Invalidated, - } - if _, err = e.Insert(comment); err != nil { - return nil, err - } - - if err = opts.Repo.GetOwner(ctx); err != nil { - return nil, err - } - - if err = updateCommentInfos(ctx, opts, comment); err != nil { - return nil, err - } - - if err = comment.addCrossReferences(ctx, opts.Doer, false); err != nil { - return nil, err - } - - return comment, nil -} - -func updateCommentInfos(ctx context.Context, opts *CreateCommentOptions, comment *Comment) (err error) { - // Check comment type. - switch opts.Type { - case CommentTypeCode: - if comment.ReviewID != 0 { - if comment.Review == nil { - if err := comment.loadReview(ctx); err != nil { - return err - } - } - if comment.Review.Type <= ReviewTypePending { - return nil - } - } - fallthrough - case CommentTypeComment: - if _, err = db.Exec(ctx, "UPDATE `issue` SET num_comments=num_comments+1 WHERE id=?", opts.Issue.ID); err != nil { - return err - } - fallthrough - case CommentTypeReview: - // Check attachments - attachments, err := repo_model.GetAttachmentsByUUIDs(ctx, opts.Attachments) - if err != nil { - return fmt.Errorf("getAttachmentsByUUIDs [uuids: %v]: %v", opts.Attachments, err) - } - - for i := range attachments { - attachments[i].IssueID = opts.Issue.ID - attachments[i].CommentID = comment.ID - // No assign value could be 0, so ignore AllCols(). - if _, err = db.GetEngine(ctx).ID(attachments[i].ID).Update(attachments[i]); err != nil { - return fmt.Errorf("update attachment [%d]: %v", attachments[i].ID, err) - } - } - case CommentTypeReopen, CommentTypeClose: - if err = updateIssueClosedNum(ctx, opts.Issue); err != nil { - return err - } - } - // update the issue's updated_unix column - return UpdateIssueCols(ctx, opts.Issue, "updated_unix") -} - -func createDeadlineComment(ctx context.Context, doer *user_model.User, issue *Issue, newDeadlineUnix timeutil.TimeStamp) (*Comment, error) { - var content string - var commentType CommentType - - // newDeadline = 0 means deleting - if newDeadlineUnix == 0 { - commentType = CommentTypeRemovedDeadline - content = issue.DeadlineUnix.Format("2006-01-02") - } else if issue.DeadlineUnix == 0 { - // Check if the new date was added or modified - // If the actual deadline is 0 => deadline added - commentType = CommentTypeAddedDeadline - content = newDeadlineUnix.Format("2006-01-02") - } else { // Otherwise modified - commentType = CommentTypeModifiedDeadline - content = newDeadlineUnix.Format("2006-01-02") + "|" + issue.DeadlineUnix.Format("2006-01-02") - } - - if err := issue.LoadRepo(ctx); err != nil { - return nil, err - } - - opts := &CreateCommentOptions{ - Type: commentType, - Doer: doer, - Repo: issue.Repo, - Issue: issue, - Content: content, - } - comment, err := CreateCommentCtx(ctx, opts) - if err != nil { - return nil, err - } - return comment, nil -} - -// Creates issue dependency comment -func createIssueDependencyComment(ctx context.Context, doer *user_model.User, issue, dependentIssue *Issue, add bool) (err error) { - cType := CommentTypeAddDependency - if !add { - cType = CommentTypeRemoveDependency - } - if err = issue.LoadRepo(ctx); err != nil { - return - } - - // Make two comments, one in each issue - opts := &CreateCommentOptions{ - Type: cType, - Doer: doer, - Repo: issue.Repo, - Issue: issue, - DependentIssueID: dependentIssue.ID, - } - if _, err = CreateCommentCtx(ctx, opts); err != nil { - return - } - - opts = &CreateCommentOptions{ - Type: cType, - Doer: doer, - Repo: issue.Repo, - Issue: dependentIssue, - DependentIssueID: issue.ID, - } - _, err = CreateCommentCtx(ctx, opts) - return -} - -// CreateCommentOptions defines options for creating comment -type CreateCommentOptions struct { - Type CommentType - Doer *user_model.User - Repo *repo_model.Repository - Issue *Issue - Label *Label - - DependentIssueID int64 - OldMilestoneID int64 - MilestoneID int64 - OldProjectID int64 - ProjectID int64 - TimeID int64 - AssigneeID int64 - AssigneeTeamID int64 - RemovedAssignee bool - OldTitle string - NewTitle string - OldRef string - NewRef string - CommitID int64 - CommitSHA string - Patch string - LineNum int64 - TreePath string - ReviewID int64 - Content string - Attachments []string // UUIDs of attachments - RefRepoID int64 - RefIssueID int64 - RefCommentID int64 - RefAction references.XRefAction - RefIsPull bool - IsForcePush bool - Invalidated bool -} - -// CreateComment creates comment of issue or commit. -func CreateComment(opts *CreateCommentOptions) (comment *Comment, err error) { - ctx, committer, err := db.TxContext() - if err != nil { - return nil, err - } - defer committer.Close() - - comment, err = CreateCommentCtx(ctx, opts) - if err != nil { - return nil, err - } - - if err = committer.Commit(); err != nil { - return nil, err - } - - return comment, nil -} - -// CreateRefComment creates a commit reference comment to issue. -func CreateRefComment(doer *user_model.User, repo *repo_model.Repository, issue *Issue, content, commitSHA string) error { - if len(commitSHA) == 0 { - return fmt.Errorf("cannot create reference with empty commit SHA") - } - - // Check if same reference from same commit has already existed. - has, err := db.GetEngine(db.DefaultContext).Get(&Comment{ - Type: CommentTypeCommitRef, - IssueID: issue.ID, - CommitSHA: commitSHA, - }) - if err != nil { - return fmt.Errorf("check reference comment: %v", err) - } else if has { - return nil - } - - _, err = CreateComment(&CreateCommentOptions{ - Type: CommentTypeCommitRef, - Doer: doer, - Repo: repo, - Issue: issue, - CommitSHA: commitSHA, - Content: content, - }) - return err -} - -// GetCommentByID returns the comment by given ID. -func GetCommentByID(ctx context.Context, id int64) (*Comment, error) { - c := new(Comment) - has, err := db.GetEngine(ctx).ID(id).Get(c) - if err != nil { - return nil, err - } else if !has { - return nil, ErrCommentNotExist{id, 0} - } - return c, nil -} - -// FindCommentsOptions describes the conditions to Find comments -type FindCommentsOptions struct { - db.ListOptions - RepoID int64 - IssueID int64 - ReviewID int64 - Since int64 - Before int64 - Line int64 - TreePath string - Type CommentType -} - -func (opts *FindCommentsOptions) toConds() builder.Cond { - cond := builder.NewCond() - if opts.RepoID > 0 { - cond = cond.And(builder.Eq{"issue.repo_id": opts.RepoID}) - } - if opts.IssueID > 0 { - cond = cond.And(builder.Eq{"comment.issue_id": opts.IssueID}) - } - if opts.ReviewID > 0 { - cond = cond.And(builder.Eq{"comment.review_id": opts.ReviewID}) - } - if opts.Since > 0 { - cond = cond.And(builder.Gte{"comment.updated_unix": opts.Since}) - } - if opts.Before > 0 { - cond = cond.And(builder.Lte{"comment.updated_unix": opts.Before}) - } - if opts.Type != CommentTypeUnknown { - cond = cond.And(builder.Eq{"comment.type": opts.Type}) - } - if opts.Line != 0 { - cond = cond.And(builder.Eq{"comment.line": opts.Line}) - } - if len(opts.TreePath) > 0 { - cond = cond.And(builder.Eq{"comment.tree_path": opts.TreePath}) - } - return cond -} - -// FindComments returns all comments according options -func FindComments(ctx context.Context, opts *FindCommentsOptions) ([]*Comment, error) { - comments := make([]*Comment, 0, 10) - sess := db.GetEngine(ctx).Where(opts.toConds()) - if opts.RepoID > 0 { - sess.Join("INNER", "issue", "issue.id = comment.issue_id") - } - - if opts.Page != 0 { - sess = db.SetSessionPagination(sess, opts) - } - - // WARNING: If you change this order you will need to fix createCodeComment - - return comments, sess. - Asc("comment.created_unix"). - Asc("comment.id"). - Find(&comments) -} - -// CountComments count all comments according options by ignoring pagination -func CountComments(opts *FindCommentsOptions) (int64, error) { - sess := db.GetEngine(db.DefaultContext).Where(opts.toConds()) - if opts.RepoID > 0 { - sess.Join("INNER", "issue", "issue.id = comment.issue_id") - } - return sess.Count(&Comment{}) -} - -// UpdateComment updates information of comment. -func UpdateComment(c *Comment, doer *user_model.User) error { - ctx, committer, err := db.TxContext() - if err != nil { - return err - } - defer committer.Close() - sess := db.GetEngine(ctx) - - if _, err := sess.ID(c.ID).AllCols().Update(c); err != nil { - return err - } - if err := c.LoadIssueCtx(ctx); err != nil { - return err - } - if err := c.addCrossReferences(ctx, doer, true); err != nil { - return err - } - if err := committer.Commit(); err != nil { - return fmt.Errorf("Commit: %v", err) - } - - return nil -} - -// 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 { - 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{ - CommentID: comment.ID, - }); err != nil { - return err - } - - if comment.Type == CommentTypeComment { - if _, err := e.ID(comment.IssueID).Decr("num_comments").Update(new(Issue)); err != nil { - return err - } - } - if _, err := e.Where("comment_id = ?", comment.ID).Cols("is_deleted").Update(&Action{IsDeleted: true}); err != nil { - return err - } - - if err := comment.neuterCrossReferences(ctx); err != nil { - return err - } - - return issues_model.DeleteReaction(ctx, &issues_model.ReactionOptions{CommentID: comment.ID}) -} - -// CodeComments represents comments on code by using this structure: FILENAME -> LINE (+ == proposed; - == previous) -> COMMENTS -type CodeComments map[string]map[int64][]*Comment - -// FetchCodeComments will return a 2d-map: ["Path"]["Line"] = Comments at line -func FetchCodeComments(ctx context.Context, issue *Issue, currentUser *user_model.User) (CodeComments, error) { - return fetchCodeCommentsByReview(ctx, issue, currentUser, nil) -} - -func fetchCodeCommentsByReview(ctx context.Context, issue *Issue, currentUser *user_model.User, review *Review) (CodeComments, error) { - pathToLineToComment := make(CodeComments) - if review == nil { - review = &Review{ID: 0} - } - opts := FindCommentsOptions{ - Type: CommentTypeCode, - IssueID: issue.ID, - ReviewID: review.ID, - } - - comments, err := findCodeComments(ctx, opts, issue, currentUser, review) - if err != nil { - return nil, err - } - - for _, comment := range comments { - if pathToLineToComment[comment.TreePath] == nil { - pathToLineToComment[comment.TreePath] = make(map[int64][]*Comment) - } - pathToLineToComment[comment.TreePath][comment.Line] = append(pathToLineToComment[comment.TreePath][comment.Line], comment) - } - return pathToLineToComment, nil -} - -func findCodeComments(ctx context.Context, opts FindCommentsOptions, issue *Issue, currentUser *user_model.User, review *Review) ([]*Comment, error) { - var comments []*Comment - if review == nil { - review = &Review{ID: 0} - } - conds := opts.toConds() - if review.ID == 0 { - conds = conds.And(builder.Eq{"invalidated": false}) - } - e := db.GetEngine(ctx) - if err := e.Where(conds). - Asc("comment.created_unix"). - Asc("comment.id"). - Find(&comments); err != nil { - return nil, err - } - - if err := issue.LoadRepo(ctx); err != nil { - return nil, err - } - - if err := CommentList(comments).loadPosters(ctx); err != nil { - return nil, err - } - - // Find all reviews by ReviewID - reviews := make(map[int64]*Review) - ids := make([]int64, 0, len(comments)) - for _, comment := range comments { - if comment.ReviewID != 0 { - ids = append(ids, comment.ReviewID) - } - } - if err := e.In("id", ids).Find(&reviews); err != nil { - return nil, err - } - - n := 0 - for _, comment := range comments { - if re, ok := reviews[comment.ReviewID]; ok && re != nil { - // If the review is pending only the author can see the comments (except if the review is set) - if review.ID == 0 && re.Type == ReviewTypePending && - (currentUser == nil || currentUser.ID != re.ReviewerID) { - continue - } - comment.Review = re - } - comments[n] = comment - n++ - - if err := comment.LoadResolveDoer(); err != nil { - return nil, err - } - - if err := comment.LoadReactions(issue.Repo); err != nil { - return nil, err - } - - var err error - if comment.RenderedContent, err = markdown.RenderString(&markup.RenderContext{ - Ctx: ctx, - URLPrefix: issue.Repo.Link(), - Metas: issue.Repo.ComposeMetas(), - }, comment.Content); err != nil { - return nil, err - } - } - return comments[:n], nil -} - -// FetchCodeCommentsByLine fetches the code comments for a given treePath and line number -func FetchCodeCommentsByLine(ctx context.Context, issue *Issue, currentUser *user_model.User, treePath string, line int64) ([]*Comment, error) { - opts := FindCommentsOptions{ - Type: CommentTypeCode, - IssueID: issue.ID, - TreePath: treePath, - Line: line, - } - return findCodeComments(ctx, opts, issue, currentUser, nil) -} - -// UpdateCommentsMigrationsByType updates comments' migrations information via given git service type and original id and poster id -func UpdateCommentsMigrationsByType(tp structs.GitServiceType, originalAuthorID string, posterID int64) error { - _, err := db.GetEngine(db.DefaultContext).Table("comment"). - Where(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, - }), - )). - And("comment.original_author_id = ?", originalAuthorID). - Update(map[string]interface{}{ - "poster_id": posterID, - "original_author": "", - "original_author_id": 0, - }) - return err -} - -// CreatePushPullComment create push code to pull base comment -func CreatePushPullComment(ctx context.Context, pusher *user_model.User, pr *PullRequest, oldCommitID, newCommitID string) (comment *Comment, err error) { - if pr.HasMerged || oldCommitID == "" || newCommitID == "" { - return nil, nil - } - - ops := &CreateCommentOptions{ - Type: CommentTypePullRequestPush, - Doer: pusher, - Repo: pr.BaseRepo, - } - - var data PushActionContent - - data.CommitIDs, data.IsForcePush, err = getCommitIDsFromRepo(ctx, pr.BaseRepo, oldCommitID, newCommitID, pr.BaseBranch) - if err != nil { - return nil, err - } - - ops.Issue = pr.Issue - - dataJSON, err := json.Marshal(data) - if err != nil { - return nil, err - } - - ops.Content = string(dataJSON) - - comment, err = CreateComment(ops) - - return -} - -// CreateAutoMergeComment is a internal function, only use it for CommentTypePRScheduledToAutoMerge and CommentTypePRUnScheduledToAutoMerge CommentTypes -func CreateAutoMergeComment(ctx context.Context, typ CommentType, pr *PullRequest, doer *user_model.User) (comment *Comment, err error) { - if typ != CommentTypePRScheduledToAutoMerge && typ != CommentTypePRUnScheduledToAutoMerge { - return nil, fmt.Errorf("comment type %d cannot be used to create an auto merge comment", typ) - } - if err = pr.LoadIssueCtx(ctx); err != nil { - return - } - - if err = pr.LoadBaseRepoCtx(ctx); err != nil { - return - } - - comment, err = CreateCommentCtx(ctx, &CreateCommentOptions{ - Type: typ, - Doer: doer, - Repo: pr.BaseRepo, - Issue: pr.Issue, - }) - return -} - -// getCommitsFromRepo get commit IDs from repo in between oldCommitID and newCommitID -// isForcePush will be true if oldCommit isn't on the branch -// Commit on baseBranch will skip -func getCommitIDsFromRepo(ctx context.Context, repo *repo_model.Repository, oldCommitID, newCommitID, baseBranch string) (commitIDs []string, isForcePush bool, err error) { - repoPath := repo.RepoPath() - gitRepo, closer, err := git.RepositoryFromContextOrOpen(ctx, repoPath) - if err != nil { - return nil, false, err - } - defer closer.Close() - - oldCommit, err := gitRepo.GetCommit(oldCommitID) - if err != nil { - return nil, false, err - } - - if err = oldCommit.LoadBranchName(); err != nil { - return nil, false, err - } - - if len(oldCommit.Branch) == 0 { - commitIDs = make([]string, 2) - commitIDs[0] = oldCommitID - commitIDs[1] = newCommitID - - return commitIDs, true, err - } - - newCommit, err := gitRepo.GetCommit(newCommitID) - if err != nil { - return nil, false, err - } - - commits, err := newCommit.CommitsBeforeUntil(oldCommitID) - if err != nil { - return nil, false, err - } - - commitIDs = make([]string, 0, len(commits)) - commitChecks := make(map[string]*commitBranchCheckItem) - - for _, commit := range commits { - commitChecks[commit.ID.String()] = &commitBranchCheckItem{ - Commit: commit, - Checked: false, - } - } - - if err = commitBranchCheck(gitRepo, newCommit, oldCommitID, baseBranch, commitChecks); err != nil { - return - } - - for i := len(commits) - 1; i >= 0; i-- { - commitID := commits[i].ID.String() - if item, ok := commitChecks[commitID]; ok && item.Checked { - commitIDs = append(commitIDs, commitID) - } - } - - return -} - -type commitBranchCheckItem struct { - Commit *git.Commit - Checked bool -} - -func commitBranchCheck(gitRepo *git.Repository, startCommit *git.Commit, endCommitID, baseBranch string, commitList map[string]*commitBranchCheckItem) error { - if startCommit.ID.String() == endCommitID { - return nil - } - - checkStack := make([]string, 0, 10) - checkStack = append(checkStack, startCommit.ID.String()) - - for len(checkStack) > 0 { - commitID := checkStack[0] - checkStack = checkStack[1:] - - item, ok := commitList[commitID] - if !ok { - continue - } - - if item.Commit.ID.String() == endCommitID { - continue - } - - if err := item.Commit.LoadBranchName(); err != nil { - return err - } - - if item.Commit.Branch == baseBranch { - continue - } - - if item.Checked { - continue - } - - item.Checked = true - - parentNum := item.Commit.ParentCount() - for i := 0; i < parentNum; i++ { - parentCommit, err := item.Commit.Parent(i) - if err != nil { - return err - } - checkStack = append(checkStack, parentCommit.ID.String()) - } - } - return nil -} - -// RemapExternalUser ExternalUserRemappable interface -func (c *Comment) RemapExternalUser(externalName string, externalID, userID int64) error { - c.OriginalAuthor = externalName - c.OriginalAuthorID = externalID - c.PosterID = userID - return nil -} - -// GetUserID ExternalUserRemappable interface -func (c *Comment) GetUserID() int64 { return c.PosterID } - -// GetExternalName ExternalUserRemappable interface -func (c *Comment) GetExternalName() string { return c.OriginalAuthor } - -// GetExternalID ExternalUserRemappable interface -func (c *Comment) GetExternalID() int64 { return c.OriginalAuthorID } diff --git a/models/issue_comment_list.go b/models/issue_comment_list.go deleted file mode 100644 index d62984c1e6..0000000000 --- a/models/issue_comment_list.go +++ /dev/null @@ -1,554 +0,0 @@ -// Copyright 2018 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" - 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" -) - -// CommentList defines a list of comments -type CommentList []*Comment - -func (comments CommentList) getPosterIDs() []int64 { - posterIDs := make(map[int64]struct{}, len(comments)) - for _, comment := range comments { - if _, ok := posterIDs[comment.PosterID]; !ok { - posterIDs[comment.PosterID] = struct{}{} - } - } - return container.KeysInt64(posterIDs) -} - -func (comments CommentList) loadPosters(ctx context.Context) error { - if len(comments) == 0 { - return nil - } - - posterIDs := comments.getPosterIDs() - posterMaps := make(map[int64]*user_model.User, len(posterIDs)) - left := len(posterIDs) - for left > 0 { - limit := defaultMaxInSize - if left < limit { - limit = left - } - err := db.GetEngine(ctx). - In("id", posterIDs[:limit]). - Find(&posterMaps) - if err != nil { - return err - } - left -= limit - posterIDs = posterIDs[limit:] - } - - for _, comment := range comments { - if comment.PosterID <= 0 { - continue - } - var ok bool - if comment.Poster, ok = posterMaps[comment.PosterID]; !ok { - comment.Poster = user_model.NewGhostUser() - } - } - return nil -} - -func (comments CommentList) getCommentIDs() []int64 { - ids := make([]int64, 0, len(comments)) - for _, comment := range comments { - ids = append(ids, comment.ID) - } - return ids -} - -func (comments CommentList) getLabelIDs() []int64 { - ids := make(map[int64]struct{}, len(comments)) - for _, comment := range comments { - if _, ok := ids[comment.LabelID]; !ok { - ids[comment.LabelID] = struct{}{} - } - } - return container.KeysInt64(ids) -} - -func (comments CommentList) loadLabels(ctx context.Context) error { - if len(comments) == 0 { - return nil - } - - labelIDs := comments.getLabelIDs() - commentLabels := make(map[int64]*Label, len(labelIDs)) - left := len(labelIDs) - for left > 0 { - limit := defaultMaxInSize - if left < limit { - limit = left - } - rows, err := db.GetEngine(ctx). - In("id", labelIDs[:limit]). - Rows(new(Label)) - if err != nil { - return err - } - - for rows.Next() { - var label Label - err = rows.Scan(&label) - if err != nil { - _ = rows.Close() - return err - } - commentLabels[label.ID] = &label - } - _ = rows.Close() - left -= limit - labelIDs = labelIDs[limit:] - } - - for _, comment := range comments { - comment.Label = commentLabels[comment.ID] - } - return nil -} - -func (comments CommentList) getMilestoneIDs() []int64 { - ids := make(map[int64]struct{}, len(comments)) - for _, comment := range comments { - if _, ok := ids[comment.MilestoneID]; !ok { - ids[comment.MilestoneID] = struct{}{} - } - } - return container.KeysInt64(ids) -} - -func (comments CommentList) loadMilestones(ctx context.Context) error { - if len(comments) == 0 { - return nil - } - - milestoneIDs := comments.getMilestoneIDs() - if len(milestoneIDs) == 0 { - return nil - } - - milestoneMaps := make(map[int64]*issues_model.Milestone, len(milestoneIDs)) - left := len(milestoneIDs) - for left > 0 { - limit := defaultMaxInSize - if left < limit { - limit = left - } - err := db.GetEngine(ctx). - In("id", milestoneIDs[:limit]). - Find(&milestoneMaps) - if err != nil { - return err - } - left -= limit - milestoneIDs = milestoneIDs[limit:] - } - - for _, issue := range comments { - issue.Milestone = milestoneMaps[issue.MilestoneID] - } - return nil -} - -func (comments CommentList) getOldMilestoneIDs() []int64 { - ids := make(map[int64]struct{}, len(comments)) - for _, comment := range comments { - if _, ok := ids[comment.OldMilestoneID]; !ok { - ids[comment.OldMilestoneID] = struct{}{} - } - } - return container.KeysInt64(ids) -} - -func (comments CommentList) loadOldMilestones(ctx context.Context) error { - if len(comments) == 0 { - return nil - } - - milestoneIDs := comments.getOldMilestoneIDs() - if len(milestoneIDs) == 0 { - return nil - } - - milestoneMaps := make(map[int64]*issues_model.Milestone, len(milestoneIDs)) - left := len(milestoneIDs) - for left > 0 { - limit := defaultMaxInSize - if left < limit { - limit = left - } - err := db.GetEngine(ctx). - In("id", milestoneIDs[:limit]). - Find(&milestoneMaps) - if err != nil { - return err - } - left -= limit - milestoneIDs = milestoneIDs[limit:] - } - - for _, issue := range comments { - issue.OldMilestone = milestoneMaps[issue.MilestoneID] - } - return nil -} - -func (comments CommentList) getAssigneeIDs() []int64 { - ids := make(map[int64]struct{}, len(comments)) - for _, comment := range comments { - if _, ok := ids[comment.AssigneeID]; !ok { - ids[comment.AssigneeID] = struct{}{} - } - } - return container.KeysInt64(ids) -} - -func (comments CommentList) loadAssignees(ctx context.Context) error { - if len(comments) == 0 { - return nil - } - - assigneeIDs := comments.getAssigneeIDs() - assignees := make(map[int64]*user_model.User, len(assigneeIDs)) - left := len(assigneeIDs) - for left > 0 { - limit := defaultMaxInSize - if left < limit { - limit = left - } - rows, err := db.GetEngine(ctx). - In("id", assigneeIDs[:limit]). - Rows(new(user_model.User)) - if err != nil { - return err - } - - for rows.Next() { - var user user_model.User - err = rows.Scan(&user) - if err != nil { - rows.Close() - return err - } - - assignees[user.ID] = &user - } - _ = rows.Close() - - left -= limit - assigneeIDs = assigneeIDs[limit:] - } - - for _, comment := range comments { - comment.Assignee = assignees[comment.AssigneeID] - } - return nil -} - -// getIssueIDs returns all the issue ids on this comment list which issue hasn't been loaded -func (comments CommentList) getIssueIDs() []int64 { - ids := make(map[int64]struct{}, len(comments)) - for _, comment := range comments { - if comment.Issue != nil { - continue - } - if _, ok := ids[comment.IssueID]; !ok { - ids[comment.IssueID] = struct{}{} - } - } - return container.KeysInt64(ids) -} - -// Issues returns all the issues of comments -func (comments CommentList) Issues() IssueList { - issues := make(map[int64]*Issue, len(comments)) - for _, comment := range comments { - if comment.Issue != nil { - if _, ok := issues[comment.Issue.ID]; !ok { - issues[comment.Issue.ID] = comment.Issue - } - } - } - - issueList := make([]*Issue, 0, len(issues)) - for _, issue := range issues { - issueList = append(issueList, issue) - } - return issueList -} - -func (comments CommentList) loadIssues(ctx context.Context) error { - if len(comments) == 0 { - return nil - } - - issueIDs := comments.getIssueIDs() - issues := make(map[int64]*Issue, len(issueIDs)) - left := len(issueIDs) - for left > 0 { - limit := defaultMaxInSize - if left < limit { - limit = left - } - rows, err := db.GetEngine(ctx). - In("id", issueIDs[:limit]). - Rows(new(Issue)) - if err != nil { - return err - } - - for rows.Next() { - var issue Issue - err = rows.Scan(&issue) - if err != nil { - rows.Close() - return err - } - - issues[issue.ID] = &issue - } - _ = rows.Close() - - left -= limit - issueIDs = issueIDs[limit:] - } - - for _, comment := range comments { - if comment.Issue == nil { - comment.Issue = issues[comment.IssueID] - } - } - return nil -} - -func (comments CommentList) getDependentIssueIDs() []int64 { - ids := make(map[int64]struct{}, len(comments)) - for _, comment := range comments { - if comment.DependentIssue != nil { - continue - } - if _, ok := ids[comment.DependentIssueID]; !ok { - ids[comment.DependentIssueID] = struct{}{} - } - } - return container.KeysInt64(ids) -} - -func (comments CommentList) loadDependentIssues(ctx context.Context) error { - if len(comments) == 0 { - return nil - } - - e := db.GetEngine(ctx) - issueIDs := comments.getDependentIssueIDs() - issues := make(map[int64]*Issue, len(issueIDs)) - left := len(issueIDs) - for left > 0 { - limit := defaultMaxInSize - if left < limit { - limit = left - } - rows, err := e. - In("id", issueIDs[:limit]). - Rows(new(Issue)) - if err != nil { - return err - } - - for rows.Next() { - var issue Issue - err = rows.Scan(&issue) - if err != nil { - _ = rows.Close() - return err - } - - issues[issue.ID] = &issue - } - _ = rows.Close() - - left -= limit - issueIDs = issueIDs[limit:] - } - - for _, comment := range comments { - if comment.DependentIssue == nil { - comment.DependentIssue = issues[comment.DependentIssueID] - if comment.DependentIssue != nil { - if err := comment.DependentIssue.LoadRepo(ctx); err != nil { - return err - } - } - } - } - return nil -} - -func (comments CommentList) loadAttachments(ctx context.Context) (err error) { - if len(comments) == 0 { - return nil - } - - attachments := make(map[int64][]*repo_model.Attachment, len(comments)) - commentsIDs := comments.getCommentIDs() - left := len(commentsIDs) - for left > 0 { - limit := defaultMaxInSize - if left < limit { - limit = left - } - rows, err := db.GetEngine(ctx).Table("attachment"). - Join("INNER", "comment", "comment.id = attachment.comment_id"). - In("comment.id", commentsIDs[:limit]). - Rows(new(repo_model.Attachment)) - if err != nil { - return err - } - - for rows.Next() { - var attachment repo_model.Attachment - err = rows.Scan(&attachment) - if err != nil { - _ = rows.Close() - return err - } - attachments[attachment.CommentID] = append(attachments[attachment.CommentID], &attachment) - } - - _ = rows.Close() - left -= limit - commentsIDs = commentsIDs[limit:] - } - - for _, comment := range comments { - comment.Attachments = attachments[comment.ID] - } - return nil -} - -func (comments CommentList) getReviewIDs() []int64 { - ids := make(map[int64]struct{}, len(comments)) - for _, comment := range comments { - if _, ok := ids[comment.ReviewID]; !ok { - ids[comment.ReviewID] = struct{}{} - } - } - return container.KeysInt64(ids) -} - -func (comments CommentList) loadReviews(ctx context.Context) error { - if len(comments) == 0 { - return nil - } - - reviewIDs := comments.getReviewIDs() - reviews := make(map[int64]*Review, len(reviewIDs)) - left := len(reviewIDs) - for left > 0 { - limit := defaultMaxInSize - if left < limit { - limit = left - } - rows, err := db.GetEngine(ctx). - In("id", reviewIDs[:limit]). - Rows(new(Review)) - if err != nil { - return err - } - - for rows.Next() { - var review Review - err = rows.Scan(&review) - if err != nil { - _ = rows.Close() - return err - } - - reviews[review.ID] = &review - } - _ = rows.Close() - - left -= limit - reviewIDs = reviewIDs[limit:] - } - - for _, comment := range comments { - comment.Review = reviews[comment.ReviewID] - } - return nil -} - -// loadAttributes loads all attributes -func (comments CommentList) loadAttributes(ctx context.Context) (err error) { - if err = comments.loadPosters(ctx); err != nil { - return - } - - if err = comments.loadLabels(ctx); err != nil { - return - } - - if err = comments.loadMilestones(ctx); err != nil { - return - } - - if err = comments.loadOldMilestones(ctx); err != nil { - return - } - - if err = comments.loadAssignees(ctx); err != nil { - return - } - - if err = comments.loadAttachments(ctx); err != nil { - return - } - - if err = comments.loadReviews(ctx); err != nil { - return - } - - if err = comments.loadIssues(ctx); err != nil { - return - } - - if err = comments.loadDependentIssues(ctx); err != nil { - return - } - - return nil -} - -// LoadAttributes loads attributes of the comments, except for attachments and -// comments -func (comments CommentList) LoadAttributes() error { - return comments.loadAttributes(db.DefaultContext) -} - -// LoadAttachments loads attachments -func (comments CommentList) LoadAttachments() error { - return comments.loadAttachments(db.DefaultContext) -} - -// LoadPosters loads posters -func (comments CommentList) LoadPosters() error { - return comments.loadPosters(db.DefaultContext) -} - -// LoadIssues loads issues of comments -func (comments CommentList) LoadIssues() error { - return comments.loadIssues(db.DefaultContext) -} diff --git a/models/issue_comment_test.go b/models/issue_comment_test.go deleted file mode 100644 index d323a08167..0000000000 --- a/models/issue_comment_test.go +++ /dev/null @@ -1,64 +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 ( - "testing" - "time" - - "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" -) - -func TestCreateComment(t *testing.T) { - assert.NoError(t, unittest.PrepareTestDatabase()) - - issue := unittest.AssertExistsAndLoadBean(t, &Issue{}).(*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, - Doer: doer, - Repo: repo, - Issue: issue, - Content: "Hello", - }) - assert.NoError(t, err) - then := time.Now().Unix() - - assert.EqualValues(t, 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) - 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) - user := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 1}).(*user_model.User) - res, err := FetchCodeComments(db.DefaultContext, issue, user) - assert.NoError(t, err) - assert.Contains(t, res, "README.md") - assert.Contains(t, res["README.md"], int64(4)) - assert.Len(t, res["README.md"][4], 1) - 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) - assert.NoError(t, err) - assert.Len(t, res, 1) -} diff --git a/models/issue_dependency.go b/models/issue_dependency.go deleted file mode 100644 index af40aa45d3..0000000000 --- a/models/issue_dependency.go +++ /dev/null @@ -1,131 +0,0 @@ -// Copyright 2018 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" - user_model "code.gitea.io/gitea/models/user" - "code.gitea.io/gitea/modules/timeutil" -) - -// IssueDependency represents an issue dependency -type IssueDependency struct { - ID int64 `xorm:"pk autoincr"` - UserID int64 `xorm:"NOT NULL"` - IssueID int64 `xorm:"UNIQUE(issue_dependency) NOT NULL"` - DependencyID int64 `xorm:"UNIQUE(issue_dependency) NOT NULL"` - CreatedUnix timeutil.TimeStamp `xorm:"created"` - UpdatedUnix timeutil.TimeStamp `xorm:"updated"` -} - -func init() { - db.RegisterModel(new(IssueDependency)) -} - -// DependencyType Defines Dependency Type Constants -type DependencyType int - -// Define Dependency Types -const ( - DependencyTypeBlockedBy DependencyType = iota - DependencyTypeBlocking -) - -// CreateIssueDependency creates a new dependency for an issue -func CreateIssueDependency(user *user_model.User, issue, dep *Issue) error { - ctx, committer, err := db.TxContext() - if err != nil { - return err - } - defer committer.Close() - - // Check if it aleready exists - exists, err := issueDepExists(ctx, issue.ID, dep.ID) - if err != nil { - return err - } - if exists { - return ErrDependencyExists{issue.ID, dep.ID} - } - // And if it would be circular - circular, err := issueDepExists(ctx, dep.ID, issue.ID) - if err != nil { - return err - } - if circular { - return ErrCircularDependency{issue.ID, dep.ID} - } - - if err := db.Insert(ctx, &IssueDependency{ - UserID: user.ID, - IssueID: issue.ID, - DependencyID: dep.ID, - }); err != nil { - return err - } - - // Add comment referencing the new dependency - if err = createIssueDependencyComment(ctx, user, issue, dep, true); err != nil { - return err - } - - return committer.Commit() -} - -// RemoveIssueDependency removes a dependency from an issue -func RemoveIssueDependency(user *user_model.User, issue, dep *Issue, depType DependencyType) (err error) { - ctx, committer, err := db.TxContext() - if err != nil { - return err - } - defer committer.Close() - - var issueDepToDelete IssueDependency - - switch depType { - case DependencyTypeBlockedBy: - issueDepToDelete = IssueDependency{IssueID: issue.ID, DependencyID: dep.ID} - case DependencyTypeBlocking: - issueDepToDelete = IssueDependency{IssueID: dep.ID, DependencyID: issue.ID} - default: - return ErrUnknownDependencyType{depType} - } - - affected, err := db.GetEngine(ctx).Delete(&issueDepToDelete) - if err != nil { - return err - } - - // If we deleted nothing, the dependency did not exist - if affected <= 0 { - return ErrDependencyNotExists{issue.ID, dep.ID} - } - - // Add comment referencing the removed dependency - if err = createIssueDependencyComment(ctx, user, issue, dep, false); err != nil { - return err - } - return committer.Commit() -} - -// Check if the dependency already exists -func issueDepExists(ctx context.Context, issueID, depID int64) (bool, error) { - return db.GetEngine(ctx).Where("(issue_id = ? AND dependency_id = ?)", issueID, depID).Exist(&IssueDependency{}) -} - -// IssueNoDependenciesLeft checks if issue can be closed -func IssueNoDependenciesLeft(ctx context.Context, issue *Issue) (bool, error) { - exists, err := db.GetEngine(ctx). - Table("issue_dependency"). - Select("issue.*"). - Join("INNER", "issue", "issue.id = issue_dependency.dependency_id"). - Where("issue_dependency.issue_id = ?", issue.ID). - And("issue.is_closed = ?", "0"). - Exist(&Issue{}) - - return !exists, err -} diff --git a/models/issue_dependency_test.go b/models/issue_dependency_test.go deleted file mode 100644 index 345a9077cd..0000000000 --- a/models/issue_dependency_test.go +++ /dev/null @@ -1,62 +0,0 @@ -// Copyright 2018 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" - - "github.com/stretchr/testify/assert" -) - -func TestCreateIssueDependency(t *testing.T) { - // Prepare - 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) - - // Create a dependency and check if it was successful - err = 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) - assert.Error(t, err) - assert.True(t, IsErrDependencyExists(err)) - - // Check for circular dependencies - err = CreateIssueDependency(user1, issue2, issue1) - assert.Error(t, err) - assert.True(t, IsErrCircularDependency(err)) - - _ = unittest.AssertExistsAndLoadBean(t, &Comment{Type: CommentTypeAddDependency, PosterID: user1.ID, IssueID: issue1.ID}) - - // Check if dependencies left is correct - left, err := IssueNoDependenciesLeft(db.DefaultContext, issue1) - assert.NoError(t, err) - assert.False(t, left) - - // Close #2 and check again - _, err = ChangeIssueStatus(db.DefaultContext, issue2, user1, true) - assert.NoError(t, err) - - left, err = IssueNoDependenciesLeft(db.DefaultContext, issue1) - assert.NoError(t, err) - assert.True(t, left) - - // Test removing the dependency - err = RemoveIssueDependency(user1, issue1, issue2, DependencyTypeBlockedBy) - assert.NoError(t, err) -} diff --git a/models/issue_label.go b/models/issue_label.go deleted file mode 100644 index 48a48dbb7c..0000000000 --- a/models/issue_label.go +++ /dev/null @@ -1,684 +0,0 @@ -// Copyright 2016 The Gogs Authors. All rights reserved. -// Copyright 2020 The Gitea Authors. -// Use of this source code is governed by a MIT-style -// license that can be found in the LICENSE file. - -package models - -import ( - "context" - "fmt" - "html/template" - "math" - "regexp" - "strconv" - "strings" - - "code.gitea.io/gitea/models/db" - user_model "code.gitea.io/gitea/models/user" - "code.gitea.io/gitea/modules/timeutil" - - "xorm.io/builder" -) - -// LabelColorPattern is a regexp witch can validate LabelColor -var LabelColorPattern = regexp.MustCompile("^#?(?:[0-9a-fA-F]{6}|[0-9a-fA-F]{3})$") - -// Label represents a label of repository for issues. -type Label struct { - ID int64 `xorm:"pk autoincr"` - RepoID int64 `xorm:"INDEX"` - OrgID int64 `xorm:"INDEX"` - Name string - Description string - Color string `xorm:"VARCHAR(7)"` - NumIssues int - NumClosedIssues int - CreatedUnix timeutil.TimeStamp `xorm:"INDEX created"` - UpdatedUnix timeutil.TimeStamp `xorm:"INDEX updated"` - - NumOpenIssues int `xorm:"-"` - NumOpenRepoIssues int64 `xorm:"-"` - IsChecked bool `xorm:"-"` - QueryString string `xorm:"-"` - IsSelected bool `xorm:"-"` - IsExcluded bool `xorm:"-"` -} - -func init() { - db.RegisterModel(new(Label)) - db.RegisterModel(new(IssueLabel)) -} - -// CalOpenIssues sets the number of open issues of a label based on the already stored number of closed issues. -func (label *Label) CalOpenIssues() { - label.NumOpenIssues = label.NumIssues - label.NumClosedIssues -} - -// CalOpenOrgIssues calculates the open issues of a label for a specific repo -func (label *Label) CalOpenOrgIssues(repoID, labelID int64) { - counts, _ := CountIssuesByRepo(&IssuesOptions{ - RepoID: repoID, - LabelIDs: []int64{labelID}, - }) - - for _, count := range counts { - label.NumOpenRepoIssues += count - } -} - -// LoadSelectedLabelsAfterClick calculates the set of selected labels when a label is clicked -func (label *Label) LoadSelectedLabelsAfterClick(currentSelectedLabels []int64) { - var labelQuerySlice []string - labelSelected := false - labelID := strconv.FormatInt(label.ID, 10) - for _, s := range currentSelectedLabels { - if s == label.ID { - labelSelected = true - } else if -s == label.ID { - labelSelected = true - label.IsExcluded = true - } else if s != 0 { - labelQuerySlice = append(labelQuerySlice, strconv.FormatInt(s, 10)) - } - } - if !labelSelected { - labelQuerySlice = append(labelQuerySlice, labelID) - } - label.IsSelected = labelSelected - label.QueryString = strings.Join(labelQuerySlice, ",") -} - -// BelongsToOrg returns true if label is an organization label -func (label *Label) BelongsToOrg() bool { - return label.OrgID > 0 -} - -// BelongsToRepo returns true if label is a repository label -func (label *Label) BelongsToRepo() bool { - return label.RepoID > 0 -} - -// SrgbToLinear converts a component of an sRGB color to its linear intensity -// See: https://en.wikipedia.org/wiki/SRGB#The_reverse_transformation_(sRGB_to_CIE_XYZ) -func SrgbToLinear(color uint8) float64 { - flt := float64(color) / 255 - if flt <= 0.04045 { - return flt / 12.92 - } - return math.Pow((flt+0.055)/1.055, 2.4) -} - -// Luminance returns the luminance of an sRGB color -func Luminance(color uint32) float64 { - r := SrgbToLinear(uint8(0xFF & (color >> 16))) - g := SrgbToLinear(uint8(0xFF & (color >> 8))) - b := SrgbToLinear(uint8(0xFF & color)) - - // luminance ratios for sRGB - return 0.2126*r + 0.7152*g + 0.0722*b -} - -// LuminanceThreshold is the luminance at which white and black appear to have the same contrast -// i.e. x such that 1.05 / (x + 0.05) = (x + 0.05) / 0.05 -// i.e. math.Sqrt(1.05*0.05) - 0.05 -const LuminanceThreshold float64 = 0.179 - -// ForegroundColor calculates the text color for labels based -// on their background color. -func (label *Label) ForegroundColor() template.CSS { - if strings.HasPrefix(label.Color, "#") { - if color, err := strconv.ParseUint(label.Color[1:], 16, 64); err == nil { - // NOTE: see web_src/js/components/ContextPopup.vue for similar implementation - luminance := Luminance(uint32(color)) - - // prefer white or black based upon contrast - if luminance < LuminanceThreshold { - return template.CSS("#fff") - } - return template.CSS("#000") - } - } - - // default to black - return template.CSS("#000") -} - -// NewLabel creates a new label -func NewLabel(ctx context.Context, label *Label) error { - if !LabelColorPattern.MatchString(label.Color) { - return fmt.Errorf("bad color code: %s", label.Color) - } - - // normalize case - label.Color = strings.ToLower(label.Color) - - // add leading hash - if label.Color[0] != '#' { - label.Color = "#" + label.Color - } - - // convert 3-character shorthand into 6-character version - if len(label.Color) == 4 { - r := label.Color[1] - g := label.Color[2] - b := label.Color[3] - label.Color = fmt.Sprintf("#%c%c%c%c%c%c", r, r, g, g, b, b) - } - - return db.Insert(ctx, label) -} - -// NewLabels creates new labels -func NewLabels(labels ...*Label) error { - ctx, committer, err := db.TxContext() - if err != nil { - return err - } - defer committer.Close() - - for _, label := range labels { - if !LabelColorPattern.MatchString(label.Color) { - return fmt.Errorf("bad color code: %s", label.Color) - } - if err := db.Insert(ctx, label); err != nil { - return err - } - } - return committer.Commit() -} - -// UpdateLabel updates label information. -func UpdateLabel(l *Label) error { - if !LabelColorPattern.MatchString(l.Color) { - return fmt.Errorf("bad color code: %s", l.Color) - } - return updateLabelCols(db.DefaultContext, l, "name", "description", "color") -} - -// DeleteLabel delete a label -func DeleteLabel(id, labelID int64) error { - label, err := GetLabelByID(db.DefaultContext, labelID) - if err != nil { - if IsErrLabelNotExist(err) { - return nil - } - return err - } - - ctx, committer, err := db.TxContext() - if err != nil { - return err - } - defer committer.Close() - - sess := db.GetEngine(ctx) - - if label.BelongsToOrg() && label.OrgID != id { - return nil - } - if label.BelongsToRepo() && label.RepoID != id { - return nil - } - - if _, err = sess.ID(labelID).Delete(new(Label)); err != nil { - return err - } else if _, err = sess. - Where("label_id = ?", labelID). - Delete(new(IssueLabel)); err != nil { - return err - } - - // delete comments about now deleted label_id - if _, err = sess.Where("label_id = ?", labelID).Cols("label_id").Delete(&Comment{}); err != nil { - return err - } - - return committer.Commit() -} - -// GetLabelByID returns a label by given ID. -func GetLabelByID(ctx context.Context, labelID int64) (*Label, error) { - if labelID <= 0 { - return nil, ErrLabelNotExist{labelID} - } - - l := &Label{} - has, err := db.GetEngine(ctx).ID(labelID).Get(l) - if err != nil { - return nil, err - } else if !has { - return nil, ErrLabelNotExist{l.ID} - } - return l, nil -} - -// GetLabelsByIDs returns a list of labels by IDs -func GetLabelsByIDs(labelIDs []int64) ([]*Label, error) { - labels := make([]*Label, 0, len(labelIDs)) - return labels, db.GetEngine(db.DefaultContext).Table("label"). - In("id", labelIDs). - Asc("name"). - Cols("id", "repo_id", "org_id"). - Find(&labels) -} - -// __________ .__ __ -// \______ \ ____ ______ ____ _____|__|/ |_ ___________ ___.__. -// | _// __ \\____ \ / _ \/ ___/ \ __\/ _ \_ __ < | | -// | | \ ___/| |_> > <_> )___ \| || | ( <_> ) | \/\___ | -// |____|_ /\___ > __/ \____/____ >__||__| \____/|__| / ____| -// \/ \/|__| \/ \/ - -// GetLabelInRepoByName returns a label by name in given repository. -func GetLabelInRepoByName(ctx context.Context, repoID int64, labelName string) (*Label, error) { - if len(labelName) == 0 || repoID <= 0 { - return nil, ErrRepoLabelNotExist{0, repoID} - } - - l := &Label{ - Name: labelName, - RepoID: repoID, - } - has, err := db.GetByBean(ctx, l) - if err != nil { - return nil, err - } else if !has { - return nil, ErrRepoLabelNotExist{0, l.RepoID} - } - return l, nil -} - -// GetLabelInRepoByID returns a label by ID in given repository. -func GetLabelInRepoByID(ctx context.Context, repoID, labelID int64) (*Label, error) { - if labelID <= 0 || repoID <= 0 { - return nil, ErrRepoLabelNotExist{labelID, repoID} - } - - l := &Label{ - ID: labelID, - RepoID: repoID, - } - has, err := db.GetByBean(ctx, l) - if err != nil { - return nil, err - } else if !has { - return nil, ErrRepoLabelNotExist{l.ID, l.RepoID} - } - return l, nil -} - -// GetLabelIDsInRepoByNames returns a list of labelIDs by names in a given -// repository. -// it silently ignores label names that do not belong to the repository. -func GetLabelIDsInRepoByNames(repoID int64, labelNames []string) ([]int64, error) { - labelIDs := make([]int64, 0, len(labelNames)) - return labelIDs, db.GetEngine(db.DefaultContext).Table("label"). - Where("repo_id = ?", repoID). - In("name", labelNames). - Asc("name"). - Cols("id"). - Find(&labelIDs) -} - -// BuildLabelNamesIssueIDsCondition returns a builder where get issue ids match label names -func BuildLabelNamesIssueIDsCondition(labelNames []string) *builder.Builder { - return builder.Select("issue_label.issue_id"). - From("issue_label"). - InnerJoin("label", "label.id = issue_label.label_id"). - Where( - builder.In("label.name", labelNames), - ). - GroupBy("issue_label.issue_id") -} - -// GetLabelsInRepoByIDs returns a list of labels by IDs in given repository, -// it silently ignores label IDs that do not belong to the repository. -func GetLabelsInRepoByIDs(repoID int64, labelIDs []int64) ([]*Label, error) { - labels := make([]*Label, 0, len(labelIDs)) - return labels, db.GetEngine(db.DefaultContext). - Where("repo_id = ?", repoID). - In("id", labelIDs). - Asc("name"). - Find(&labels) -} - -// GetLabelsByRepoID returns all labels that belong to given repository by ID. -func GetLabelsByRepoID(ctx context.Context, repoID int64, sortType string, listOptions db.ListOptions) ([]*Label, error) { - if repoID <= 0 { - return nil, ErrRepoLabelNotExist{0, repoID} - } - labels := make([]*Label, 0, 10) - sess := db.GetEngine(ctx).Where("repo_id = ?", repoID) - - switch sortType { - case "reversealphabetically": - sess.Desc("name") - case "leastissues": - sess.Asc("num_issues") - case "mostissues": - sess.Desc("num_issues") - default: - sess.Asc("name") - } - - if listOptions.Page != 0 { - sess = db.SetSessionPagination(sess, &listOptions) - } - - return labels, sess.Find(&labels) -} - -// CountLabelsByRepoID count number of all labels that belong to given repository by ID. -func CountLabelsByRepoID(repoID int64) (int64, error) { - return db.GetEngine(db.DefaultContext).Where("repo_id = ?", repoID).Count(&Label{}) -} - -// ________ -// \_____ \_______ ____ -// / | \_ __ \/ ___\ -// / | \ | \/ /_/ > -// \_______ /__| \___ / -// \/ /_____/ - -// GetLabelInOrgByName returns a label by name in given organization. -func GetLabelInOrgByName(ctx context.Context, orgID int64, labelName string) (*Label, error) { - if len(labelName) == 0 || orgID <= 0 { - return nil, ErrOrgLabelNotExist{0, orgID} - } - - l := &Label{ - Name: labelName, - OrgID: orgID, - } - has, err := db.GetByBean(ctx, l) - if err != nil { - return nil, err - } else if !has { - return nil, ErrOrgLabelNotExist{0, l.OrgID} - } - return l, nil -} - -// GetLabelInOrgByID returns a label by ID in given organization. -func GetLabelInOrgByID(ctx context.Context, orgID, labelID int64) (*Label, error) { - if labelID <= 0 || orgID <= 0 { - return nil, ErrOrgLabelNotExist{labelID, orgID} - } - - l := &Label{ - ID: labelID, - OrgID: orgID, - } - has, err := db.GetByBean(ctx, l) - if err != nil { - return nil, err - } else if !has { - return nil, ErrOrgLabelNotExist{l.ID, l.OrgID} - } - return l, nil -} - -// GetLabelIDsInOrgByNames returns a list of labelIDs by names in a given -// organization. -func GetLabelIDsInOrgByNames(orgID int64, labelNames []string) ([]int64, error) { - if orgID <= 0 { - return nil, ErrOrgLabelNotExist{0, orgID} - } - labelIDs := make([]int64, 0, len(labelNames)) - - return labelIDs, db.GetEngine(db.DefaultContext).Table("label"). - Where("org_id = ?", orgID). - In("name", labelNames). - Asc("name"). - Cols("id"). - Find(&labelIDs) -} - -// GetLabelsInOrgByIDs returns a list of labels by IDs in given organization, -// it silently ignores label IDs that do not belong to the organization. -func GetLabelsInOrgByIDs(orgID int64, labelIDs []int64) ([]*Label, error) { - labels := make([]*Label, 0, len(labelIDs)) - return labels, db.GetEngine(db.DefaultContext). - Where("org_id = ?", orgID). - In("id", labelIDs). - Asc("name"). - Find(&labels) -} - -// GetLabelsByOrgID returns all labels that belong to given organization by ID. -func GetLabelsByOrgID(ctx context.Context, orgID int64, sortType string, listOptions db.ListOptions) ([]*Label, error) { - if orgID <= 0 { - return nil, ErrOrgLabelNotExist{0, orgID} - } - labels := make([]*Label, 0, 10) - sess := db.GetEngine(ctx).Where("org_id = ?", orgID) - - switch sortType { - case "reversealphabetically": - sess.Desc("name") - case "leastissues": - sess.Asc("num_issues") - case "mostissues": - sess.Desc("num_issues") - default: - sess.Asc("name") - } - - if listOptions.Page != 0 { - sess = db.SetSessionPagination(sess, &listOptions) - } - - return labels, sess.Find(&labels) -} - -// CountLabelsByOrgID count all labels that belong to given organization by ID. -func CountLabelsByOrgID(orgID int64) (int64, error) { - return db.GetEngine(db.DefaultContext).Where("org_id = ?", orgID).Count(&Label{}) -} - -// .___ -// | | ______ ________ __ ____ -// | |/ ___// ___/ | \_/ __ \ -// | |\___ \ \___ \| | /\ ___/ -// |___/____ >____ >____/ \___ | -// \/ \/ \/ - -// GetLabelsByIssueID returns all labels that belong to given issue by ID. -func GetLabelsByIssueID(ctx context.Context, issueID int64) ([]*Label, error) { - var labels []*Label - return labels, db.GetEngine(ctx).Where("issue_label.issue_id = ?", issueID). - Join("LEFT", "issue_label", "issue_label.label_id = label.id"). - Asc("label.name"). - Find(&labels) -} - -func updateLabelCols(ctx context.Context, l *Label, cols ...string) error { - _, err := db.GetEngine(ctx).ID(l.ID). - SetExpr("num_issues", - builder.Select("count(*)").From("issue_label"). - Where(builder.Eq{"label_id": l.ID}), - ). - SetExpr("num_closed_issues", - builder.Select("count(*)").From("issue_label"). - InnerJoin("issue", "issue_label.issue_id = issue.id"). - Where(builder.Eq{ - "issue_label.label_id": l.ID, - "issue.is_closed": true, - }), - ). - Cols(cols...).Update(l) - return err -} - -// .___ .____ ___. .__ -// | | ______ ________ __ ____ | | _____ \_ |__ ____ | | -// | |/ ___// ___/ | \_/ __ \| | \__ \ | __ \_/ __ \| | -// | |\___ \ \___ \| | /\ ___/| |___ / __ \| \_\ \ ___/| |__ -// |___/____ >____ >____/ \___ >_______ (____ /___ /\___ >____/ -// \/ \/ \/ \/ \/ \/ \/ - -// IssueLabel represents an issue-label relation. -type IssueLabel struct { - ID int64 `xorm:"pk autoincr"` - IssueID int64 `xorm:"UNIQUE(s)"` - LabelID int64 `xorm:"UNIQUE(s)"` -} - -// HasIssueLabel returns true if issue has been labeled. -func HasIssueLabel(ctx context.Context, issueID, labelID int64) bool { - has, _ := db.GetEngine(ctx).Where("issue_id = ? AND label_id = ?", issueID, labelID).Get(new(IssueLabel)) - return has -} - -// newIssueLabel this function creates a new label it does not check if the label is valid for the issue -// YOU MUST CHECK THIS BEFORE THIS FUNCTION -func newIssueLabel(ctx context.Context, issue *Issue, label *Label, doer *user_model.User) (err error) { - if err = db.Insert(ctx, &IssueLabel{ - IssueID: issue.ID, - LabelID: label.ID, - }); err != nil { - return err - } - - if err = issue.LoadRepo(ctx); err != nil { - return - } - - opts := &CreateCommentOptions{ - Type: CommentTypeLabel, - Doer: doer, - Repo: issue.Repo, - Issue: issue, - Label: label, - Content: "1", - } - if _, err = CreateCommentCtx(ctx, opts); err != nil { - return err - } - - return updateLabelCols(ctx, label, "num_issues", "num_closed_issue") -} - -// NewIssueLabel creates a new issue-label relation. -func NewIssueLabel(issue *Issue, label *Label, doer *user_model.User) (err error) { - if HasIssueLabel(db.DefaultContext, issue.ID, label.ID) { - return nil - } - - ctx, committer, err := db.TxContext() - if err != nil { - return err - } - defer committer.Close() - - if err = issue.LoadRepo(ctx); err != nil { - return err - } - - // Do NOT add invalid labels - if issue.RepoID != label.RepoID && issue.Repo.OwnerID != label.OrgID { - return nil - } - - if err = newIssueLabel(ctx, issue, label, doer); err != nil { - return err - } - - issue.Labels = nil - if err = issue.LoadLabels(ctx); err != nil { - return err - } - - return committer.Commit() -} - -// newIssueLabels add labels to an issue. It will check if the labels are valid for the issue -func newIssueLabels(ctx context.Context, issue *Issue, labels []*Label, doer *user_model.User) (err error) { - if err = issue.LoadRepo(ctx); err != nil { - return err - } - for _, label := range labels { - // Don't add already present labels and invalid labels - if HasIssueLabel(ctx, issue.ID, label.ID) || - (label.RepoID != issue.RepoID && label.OrgID != issue.Repo.OwnerID) { - continue - } - - if err = newIssueLabel(ctx, issue, label, doer); err != nil { - return fmt.Errorf("newIssueLabel: %v", err) - } - } - - return nil -} - -// NewIssueLabels creates a list of issue-label relations. -func NewIssueLabels(issue *Issue, labels []*Label, doer *user_model.User) (err error) { - ctx, committer, err := db.TxContext() - if err != nil { - return err - } - defer committer.Close() - - if err = newIssueLabels(ctx, issue, labels, doer); err != nil { - return err - } - - issue.Labels = nil - if err = issue.LoadLabels(ctx); err != nil { - return err - } - - return committer.Commit() -} - -func deleteIssueLabel(ctx context.Context, issue *Issue, label *Label, doer *user_model.User) (err error) { - if count, err := db.DeleteByBean(ctx, &IssueLabel{ - IssueID: issue.ID, - LabelID: label.ID, - }); err != nil { - return err - } else if count == 0 { - return nil - } - - if err = issue.LoadRepo(ctx); err != nil { - return - } - - opts := &CreateCommentOptions{ - Type: CommentTypeLabel, - Doer: doer, - Repo: issue.Repo, - Issue: issue, - Label: label, - } - if _, err = CreateCommentCtx(ctx, opts); err != nil { - return err - } - - return updateLabelCols(ctx, label, "num_issues", "num_closed_issue") -} - -// DeleteIssueLabel deletes issue-label relation. -func DeleteIssueLabel(ctx context.Context, issue *Issue, label *Label, doer *user_model.User) error { - if err := deleteIssueLabel(ctx, issue, label, doer); err != nil { - return err - } - - issue.Labels = nil - return issue.LoadLabels(ctx) -} - -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). - Delete(&IssueLabel{}); err != nil { - return err - } - - _, err := db.DeleteByBean(ctx, &Label{RepoID: repoID}) - return err -} 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_list.go b/models/issue_list.go deleted file mode 100644 index a5fc095e12..0000000000 --- a/models/issue_list.go +++ /dev/null @@ -1,571 +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 ( - "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" - - "xorm.io/builder" -) - -// 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)) - for _, issue := range issues { - if issue.Repo == nil { - repoIDs[issue.RepoID] = struct{}{} - } - if issue.PullRequest != nil && issue.PullRequest.HeadRepo == nil { - repoIDs[issue.PullRequest.HeadRepoID] = struct{}{} - } - } - return container.KeysInt64(repoIDs) -} - -func (issues IssueList) loadRepositories(ctx context.Context) ([]*repo_model.Repository, error) { - if len(issues) == 0 { - return nil, nil - } - - repoIDs := issues.getRepoIDs() - repoMaps := make(map[int64]*repo_model.Repository, len(repoIDs)) - left := len(repoIDs) - for left > 0 { - limit := defaultMaxInSize - if left < limit { - limit = left - } - err := db.GetEngine(ctx). - In("id", repoIDs[:limit]). - Find(&repoMaps) - if err != nil { - return nil, fmt.Errorf("find repository: %v", err) - } - left -= limit - repoIDs = repoIDs[limit:] - } - - for _, issue := range issues { - if issue.Repo == nil { - issue.Repo = repoMaps[issue.RepoID] - } else { - repoMaps[issue.RepoID] = issue.Repo - } - if issue.PullRequest != nil { - issue.PullRequest.BaseRepo = issue.Repo - if issue.PullRequest.HeadRepo == nil { - issue.PullRequest.HeadRepo = repoMaps[issue.PullRequest.HeadRepoID] - } - } - } - return repo_model.ValuesRepository(repoMaps), nil -} - -// LoadRepositories loads issues' all repositories -func (issues IssueList) LoadRepositories() ([]*repo_model.Repository, error) { - return issues.loadRepositories(db.DefaultContext) -} - -func (issues IssueList) getPosterIDs() []int64 { - posterIDs := make(map[int64]struct{}, len(issues)) - for _, issue := range issues { - if _, ok := posterIDs[issue.PosterID]; !ok { - posterIDs[issue.PosterID] = struct{}{} - } - } - return container.KeysInt64(posterIDs) -} - -func (issues IssueList) loadPosters(ctx context.Context) error { - if len(issues) == 0 { - return nil - } - - posterIDs := issues.getPosterIDs() - posterMaps := make(map[int64]*user_model.User, len(posterIDs)) - left := len(posterIDs) - for left > 0 { - limit := defaultMaxInSize - if left < limit { - limit = left - } - err := db.GetEngine(ctx). - In("id", posterIDs[:limit]). - Find(&posterMaps) - if err != nil { - return err - } - left -= limit - posterIDs = posterIDs[limit:] - } - - for _, issue := range issues { - if issue.PosterID <= 0 { - continue - } - var ok bool - if issue.Poster, ok = posterMaps[issue.PosterID]; !ok { - issue.Poster = user_model.NewGhostUser() - } - } - return nil -} - -func (issues IssueList) getIssueIDs() []int64 { - ids := make([]int64, 0, len(issues)) - for _, issue := range issues { - ids = append(ids, issue.ID) - } - return ids -} - -func (issues IssueList) loadLabels(ctx context.Context) error { - if len(issues) == 0 { - return nil - } - - type LabelIssue struct { - Label *Label `xorm:"extends"` - IssueLabel *IssueLabel `xorm:"extends"` - } - - issueLabels := make(map[int64][]*Label, len(issues)*3) - issueIDs := issues.getIssueIDs() - left := len(issueIDs) - for left > 0 { - limit := defaultMaxInSize - if left < limit { - limit = left - } - rows, err := db.GetEngine(ctx).Table("label"). - Join("LEFT", "issue_label", "issue_label.label_id = label.id"). - In("issue_label.issue_id", issueIDs[:limit]). - Asc("label.name"). - Rows(new(LabelIssue)) - if err != nil { - return err - } - - for rows.Next() { - var labelIssue LabelIssue - err = rows.Scan(&labelIssue) - if err != nil { - if err1 := rows.Close(); err1 != nil { - return fmt.Errorf("IssueList.loadLabels: Close: %v", err1) - } - return err - } - issueLabels[labelIssue.IssueLabel.IssueID] = append(issueLabels[labelIssue.IssueLabel.IssueID], labelIssue.Label) - } - // When there are no rows left and we try to close it. - // Since that is not relevant for us, we can safely ignore it. - if err1 := rows.Close(); err1 != nil { - return fmt.Errorf("IssueList.loadLabels: Close: %v", err1) - } - left -= limit - issueIDs = issueIDs[limit:] - } - - for _, issue := range issues { - issue.Labels = issueLabels[issue.ID] - } - return nil -} - -func (issues IssueList) getMilestoneIDs() []int64 { - ids := make(map[int64]struct{}, len(issues)) - for _, issue := range issues { - if _, ok := ids[issue.MilestoneID]; !ok { - ids[issue.MilestoneID] = struct{}{} - } - } - return container.KeysInt64(ids) -} - -func (issues IssueList) loadMilestones(ctx context.Context) error { - milestoneIDs := issues.getMilestoneIDs() - if len(milestoneIDs) == 0 { - return nil - } - - milestoneMaps := make(map[int64]*issues_model.Milestone, len(milestoneIDs)) - left := len(milestoneIDs) - for left > 0 { - limit := defaultMaxInSize - if left < limit { - limit = left - } - err := db.GetEngine(ctx). - In("id", milestoneIDs[:limit]). - Find(&milestoneMaps) - if err != nil { - return err - } - left -= limit - milestoneIDs = milestoneIDs[limit:] - } - - for _, issue := range issues { - issue.Milestone = milestoneMaps[issue.MilestoneID] - } - return nil -} - -func (issues IssueList) loadAssignees(ctx context.Context) error { - if len(issues) == 0 { - return nil - } - - type AssigneeIssue struct { - IssueAssignee *IssueAssignees `xorm:"extends"` - Assignee *user_model.User `xorm:"extends"` - } - - assignees := make(map[int64][]*user_model.User, len(issues)) - issueIDs := issues.getIssueIDs() - left := len(issueIDs) - for left > 0 { - limit := defaultMaxInSize - if left < limit { - limit = left - } - rows, err := db.GetEngine(ctx).Table("issue_assignees"). - Join("INNER", "`user`", "`user`.id = `issue_assignees`.assignee_id"). - In("`issue_assignees`.issue_id", issueIDs[:limit]). - Rows(new(AssigneeIssue)) - if err != nil { - return err - } - - for rows.Next() { - var assigneeIssue AssigneeIssue - err = rows.Scan(&assigneeIssue) - if err != nil { - if err1 := rows.Close(); err1 != nil { - return fmt.Errorf("IssueList.loadAssignees: Close: %v", err1) - } - return err - } - - assignees[assigneeIssue.IssueAssignee.IssueID] = append(assignees[assigneeIssue.IssueAssignee.IssueID], assigneeIssue.Assignee) - } - if err1 := rows.Close(); err1 != nil { - return fmt.Errorf("IssueList.loadAssignees: Close: %v", err1) - } - left -= limit - issueIDs = issueIDs[limit:] - } - - for _, issue := range issues { - issue.Assignees = assignees[issue.ID] - } - return nil -} - -func (issues IssueList) getPullIssueIDs() []int64 { - ids := make([]int64, 0, len(issues)) - for _, issue := range issues { - if issue.IsPull && issue.PullRequest == nil { - ids = append(ids, issue.ID) - } - } - return ids -} - -func (issues IssueList) loadPullRequests(ctx context.Context) error { - issuesIDs := issues.getPullIssueIDs() - if len(issuesIDs) == 0 { - return nil - } - - pullRequestMaps := make(map[int64]*PullRequest, len(issuesIDs)) - left := len(issuesIDs) - for left > 0 { - limit := defaultMaxInSize - if left < limit { - limit = left - } - rows, err := db.GetEngine(ctx). - In("issue_id", issuesIDs[:limit]). - Rows(new(PullRequest)) - if err != nil { - return err - } - - for rows.Next() { - var pr PullRequest - err = rows.Scan(&pr) - if err != nil { - if err1 := rows.Close(); err1 != nil { - return fmt.Errorf("IssueList.loadPullRequests: Close: %v", err1) - } - return err - } - pullRequestMaps[pr.IssueID] = &pr - } - if err1 := rows.Close(); err1 != nil { - return fmt.Errorf("IssueList.loadPullRequests: Close: %v", err1) - } - left -= limit - issuesIDs = issuesIDs[limit:] - } - - for _, issue := range issues { - issue.PullRequest = pullRequestMaps[issue.ID] - } - return nil -} - -func (issues IssueList) loadAttachments(ctx context.Context) (err error) { - if len(issues) == 0 { - return nil - } - - attachments := make(map[int64][]*repo_model.Attachment, len(issues)) - issuesIDs := issues.getIssueIDs() - left := len(issuesIDs) - for left > 0 { - limit := defaultMaxInSize - if left < limit { - limit = left - } - rows, err := db.GetEngine(ctx).Table("attachment"). - Join("INNER", "issue", "issue.id = attachment.issue_id"). - In("issue.id", issuesIDs[:limit]). - Rows(new(repo_model.Attachment)) - if err != nil { - return err - } - - for rows.Next() { - var attachment repo_model.Attachment - err = rows.Scan(&attachment) - if err != nil { - if err1 := rows.Close(); err1 != nil { - return fmt.Errorf("IssueList.loadAttachments: Close: %v", err1) - } - return err - } - attachments[attachment.IssueID] = append(attachments[attachment.IssueID], &attachment) - } - if err1 := rows.Close(); err1 != nil { - return fmt.Errorf("IssueList.loadAttachments: Close: %v", err1) - } - left -= limit - issuesIDs = issuesIDs[limit:] - } - - for _, issue := range issues { - issue.Attachments = attachments[issue.ID] - } - return nil -} - -func (issues IssueList) loadComments(ctx context.Context, cond builder.Cond) (err error) { - if len(issues) == 0 { - return nil - } - - comments := make(map[int64][]*Comment, len(issues)) - issuesIDs := issues.getIssueIDs() - left := len(issuesIDs) - for left > 0 { - limit := defaultMaxInSize - if left < limit { - limit = left - } - rows, err := db.GetEngine(ctx).Table("comment"). - Join("INNER", "issue", "issue.id = comment.issue_id"). - In("issue.id", issuesIDs[:limit]). - Where(cond). - Rows(new(Comment)) - if err != nil { - return err - } - - for rows.Next() { - var comment Comment - err = rows.Scan(&comment) - if err != nil { - if err1 := rows.Close(); err1 != nil { - return fmt.Errorf("IssueList.loadComments: Close: %v", err1) - } - return err - } - comments[comment.IssueID] = append(comments[comment.IssueID], &comment) - } - if err1 := rows.Close(); err1 != nil { - return fmt.Errorf("IssueList.loadComments: Close: %v", err1) - } - left -= limit - issuesIDs = issuesIDs[limit:] - } - - for _, issue := range issues { - issue.Comments = comments[issue.ID] - } - return nil -} - -func (issues IssueList) loadTotalTrackedTimes(ctx context.Context) (err error) { - type totalTimesByIssue struct { - IssueID int64 - Time int64 - } - if len(issues) == 0 { - return nil - } - trackedTimes := make(map[int64]int64, len(issues)) - - ids := make([]int64, 0, len(issues)) - for _, issue := range issues { - if issue.Repo.IsTimetrackerEnabled() { - ids = append(ids, issue.ID) - } - } - - left := len(ids) - for left > 0 { - limit := defaultMaxInSize - if left < limit { - limit = left - } - - // select issue_id, sum(time) from tracked_time where issue_id in () group by issue_id - rows, err := db.GetEngine(ctx).Table("tracked_time"). - Where("deleted = ?", false). - Select("issue_id, sum(time) as time"). - In("issue_id", ids[:limit]). - GroupBy("issue_id"). - Rows(new(totalTimesByIssue)) - if err != nil { - return err - } - - for rows.Next() { - var totalTime totalTimesByIssue - err = rows.Scan(&totalTime) - if err != nil { - if err1 := rows.Close(); err1 != nil { - return fmt.Errorf("IssueList.loadTotalTrackedTimes: Close: %v", err1) - } - return err - } - trackedTimes[totalTime.IssueID] = totalTime.Time - } - if err1 := rows.Close(); err1 != nil { - return fmt.Errorf("IssueList.loadTotalTrackedTimes: Close: %v", err1) - } - left -= limit - ids = ids[limit:] - } - - for _, issue := range issues { - issue.TotalTrackedTime = trackedTimes[issue.ID] - } - return nil -} - -// loadAttributes loads all attributes, expect for attachments and comments -func (issues IssueList) loadAttributes(ctx context.Context) error { - if _, err := issues.loadRepositories(ctx); err != nil { - return fmt.Errorf("issue.loadAttributes: loadRepositories: %v", err) - } - - if err := issues.loadPosters(ctx); err != nil { - return fmt.Errorf("issue.loadAttributes: loadPosters: %v", err) - } - - if err := issues.loadLabels(ctx); err != nil { - return fmt.Errorf("issue.loadAttributes: loadLabels: %v", err) - } - - if err := issues.loadMilestones(ctx); err != nil { - return fmt.Errorf("issue.loadAttributes: loadMilestones: %v", err) - } - - if err := issues.loadAssignees(ctx); err != nil { - return fmt.Errorf("issue.loadAttributes: loadAssignees: %v", err) - } - - if err := issues.loadPullRequests(ctx); err != nil { - return fmt.Errorf("issue.loadAttributes: loadPullRequests: %v", err) - } - - if err := issues.loadTotalTrackedTimes(ctx); err != nil { - return fmt.Errorf("issue.loadAttributes: loadTotalTrackedTimes: %v", err) - } - - return nil -} - -// LoadAttributes loads attributes of the issues, except for attachments and -// comments -func (issues IssueList) LoadAttributes() error { - return issues.loadAttributes(db.DefaultContext) -} - -// LoadAttachments loads attachments -func (issues IssueList) LoadAttachments() error { - return issues.loadAttachments(db.DefaultContext) -} - -// LoadComments loads comments -func (issues IssueList) LoadComments() error { - return issues.loadComments(db.DefaultContext, builder.NewCond()) -} - -// LoadDiscussComments loads discuss comments -func (issues IssueList) LoadDiscussComments() error { - return issues.loadComments(db.DefaultContext, builder.Eq{"comment.type": CommentTypeComment}) -} - -// LoadPullRequests loads pull requests -func (issues IssueList) LoadPullRequests() error { - return issues.loadPullRequests(db.DefaultContext) -} - -// GetApprovalCounts returns a map of issue ID to slice of approval counts -// FIXME: only returns official counts due to double counting of non-official approvals -func (issues IssueList) GetApprovalCounts(ctx context.Context) (map[int64][]*ReviewCount, error) { - rCounts := make([]*ReviewCount, 0, 2*len(issues)) - ids := make([]int64, len(issues)) - for i, issue := range issues { - ids[i] = issue.ID - } - sess := db.GetEngine(ctx).In("issue_id", ids) - err := sess.Select("issue_id, type, count(id) as `count`"). - Where("official = ? AND dismissed = ?", true, false). - GroupBy("issue_id, type"). - OrderBy("issue_id"). - Table("review"). - Find(&rCounts) - if err != nil { - return nil, err - } - - approvalCountMap := make(map[int64][]*ReviewCount, len(issues)) - - for _, c := range rCounts { - approvalCountMap[c.IssueID] = append(approvalCountMap[c.IssueID], c) - } - - return approvalCountMap, nil -} diff --git a/models/issue_list_test.go b/models/issue_list_test.go deleted file mode 100644 index 2916a7302f..0000000000 --- a/models/issue_list_test.go +++ /dev/null @@ -1,72 +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 ( - "testing" - - "code.gitea.io/gitea/models/unittest" - "code.gitea.io/gitea/modules/setting" - - "github.com/stretchr/testify/assert" -) - -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), - } - - repos, err := issueList.LoadRepositories() - assert.NoError(t, err) - assert.Len(t, repos, 2) - for _, issue := range issueList { - assert.EqualValues(t, issue.RepoID, issue.Repo.ID) - } -} - -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), - } - - assert.NoError(t, issueList.LoadAttributes()) - for _, issue := range issueList { - 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}) - } - if issue.PosterID > 0 { - assert.EqualValues(t, issue.PosterID, issue.Poster.ID) - } - if issue.AssigneeID > 0 { - assert.EqualValues(t, issue.AssigneeID, issue.Assignee.ID) - } - if issue.MilestoneID > 0 { - assert.EqualValues(t, issue.MilestoneID, issue.Milestone.ID) - } - if issue.IsPull { - assert.EqualValues(t, issue.ID, issue.PullRequest.IssueID) - } - for _, attachment := range issue.Attachments { - assert.EqualValues(t, issue.ID, attachment.IssueID) - } - for _, comment := range issue.Comments { - assert.EqualValues(t, issue.ID, comment.IssueID) - } - if issue.ID == int64(1) { - assert.Equal(t, int64(400), issue.TotalTrackedTime) - } else if issue.ID == int64(2) { - assert.Equal(t, int64(3682), issue.TotalTrackedTime) - } - } -} diff --git a/models/issue_lock.go b/models/issue_lock.go deleted file mode 100644 index a122f618d0..0000000000 --- a/models/issue_lock.go +++ /dev/null @@ -1,65 +0,0 @@ -// Copyright 2019 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 ( - "code.gitea.io/gitea/models/db" - user_model "code.gitea.io/gitea/models/user" -) - -// IssueLockOptions defines options for locking and/or unlocking an issue/PR -type IssueLockOptions struct { - Doer *user_model.User - Issue *Issue - Reason string -} - -// LockIssue locks an issue. This would limit commenting abilities to -// users with write access to the repo -func LockIssue(opts *IssueLockOptions) error { - return updateIssueLock(opts, true) -} - -// UnlockIssue unlocks a previously locked issue. -func UnlockIssue(opts *IssueLockOptions) error { - return updateIssueLock(opts, false) -} - -func updateIssueLock(opts *IssueLockOptions, lock bool) error { - if opts.Issue.IsLocked == lock { - return nil - } - - opts.Issue.IsLocked = lock - var commentType CommentType - if opts.Issue.IsLocked { - commentType = CommentTypeLock - } else { - commentType = CommentTypeUnlock - } - - ctx, committer, err := db.TxContext() - if err != nil { - return err - } - defer committer.Close() - - if err := UpdateIssueCols(ctx, opts.Issue, "is_locked"); err != nil { - return err - } - - opt := &CreateCommentOptions{ - Doer: opts.Doer, - Issue: opts.Issue, - Repo: opts.Issue.Repo, - Type: commentType, - Content: opts.Reason, - } - if _, err := CreateCommentCtx(ctx, opt); err != nil { - return err - } - - return committer.Commit() -} diff --git a/models/issue_project.go b/models/issue_project.go deleted file mode 100644 index 0f8c61977b..0000000000 --- a/models/issue_project.go +++ /dev/null @@ -1,179 +0,0 @@ -// Copyright 2021 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" - "fmt" - - "code.gitea.io/gitea/models/db" - project_model "code.gitea.io/gitea/models/project" - user_model "code.gitea.io/gitea/models/user" -) - -// LoadProject load the project the issue was assigned to -func (i *Issue) LoadProject() (err error) { - return i.loadProject(db.DefaultContext) -} - -func (i *Issue) loadProject(ctx context.Context) (err error) { - if i.Project == nil { - var p project_model.Project - if _, err = db.GetEngine(ctx).Table("project"). - Join("INNER", "project_issue", "project.id=project_issue.project_id"). - Where("project_issue.issue_id = ?", i.ID). - Get(&p); err != nil { - return err - } - i.Project = &p - } - return -} - -// ProjectID return project id if issue was assigned to one -func (i *Issue) ProjectID() int64 { - return i.projectID(db.DefaultContext) -} - -func (i *Issue) projectID(ctx context.Context) int64 { - var ip project_model.ProjectIssue - has, err := db.GetEngine(ctx).Where("issue_id=?", i.ID).Get(&ip) - if err != nil || !has { - return 0 - } - return ip.ProjectID -} - -// ProjectBoardID return project board id if issue was assigned to one -func (i *Issue) ProjectBoardID() int64 { - return i.projectBoardID(db.DefaultContext) -} - -func (i *Issue) projectBoardID(ctx context.Context) int64 { - var ip project_model.ProjectIssue - has, err := db.GetEngine(ctx).Where("issue_id=?", i.ID).Get(&ip) - if err != nil || !has { - return 0 - } - return ip.ProjectBoardID -} - -// LoadIssuesFromBoard load issues assigned to this board -func LoadIssuesFromBoard(b *project_model.Board) (IssueList, error) { - issueList := make([]*Issue, 0, 10) - - if b.ID != 0 { - issues, err := Issues(&IssuesOptions{ - ProjectBoardID: b.ID, - ProjectID: b.ProjectID, - }) - if err != nil { - return nil, err - } - issueList = issues - } - - if b.Default { - issues, err := Issues(&IssuesOptions{ - ProjectBoardID: -1, // Issues without ProjectBoardID - ProjectID: b.ProjectID, - }) - if err != nil { - return nil, err - } - issueList = append(issueList, issues...) - } - - if err := IssueList(issueList).LoadComments(); err != nil { - return nil, err - } - - return issueList, nil -} - -// LoadIssuesFromBoardList load issues assigned to the boards -func LoadIssuesFromBoardList(bs project_model.BoardList) (map[int64]IssueList, error) { - issuesMap := make(map[int64]IssueList, len(bs)) - for i := range bs { - il, err := LoadIssuesFromBoard(bs[i]) - if err != nil { - return nil, err - } - issuesMap[bs[i].ID] = il - } - return issuesMap, nil -} - -// ChangeProjectAssign changes the project associated with an issue -func ChangeProjectAssign(issue *Issue, doer *user_model.User, newProjectID int64) error { - ctx, committer, err := db.TxContext() - if err != nil { - return err - } - defer committer.Close() - - if err := addUpdateIssueProject(ctx, issue, doer, newProjectID); err != nil { - return err - } - - return committer.Commit() -} - -func addUpdateIssueProject(ctx context.Context, issue *Issue, doer *user_model.User, newProjectID int64) error { - oldProjectID := issue.projectID(ctx) - - if _, err := db.GetEngine(ctx).Where("project_issue.issue_id=?", issue.ID).Delete(&project_model.ProjectIssue{}); err != nil { - return err - } - - if err := issue.LoadRepo(ctx); err != nil { - return err - } - - if oldProjectID > 0 || newProjectID > 0 { - if _, err := CreateCommentCtx(ctx, &CreateCommentOptions{ - Type: CommentTypeProject, - Doer: doer, - Repo: issue.Repo, - Issue: issue, - OldProjectID: oldProjectID, - ProjectID: newProjectID, - }); err != nil { - return err - } - } - - return db.Insert(ctx, &project_model.ProjectIssue{ - IssueID: issue.ID, - ProjectID: newProjectID, - }) -} - -// MoveIssueAcrossProjectBoards move a card from one board to another -func MoveIssueAcrossProjectBoards(issue *Issue, board *project_model.Board) error { - ctx, committer, err := db.TxContext() - if err != nil { - return err - } - defer committer.Close() - sess := db.GetEngine(ctx) - - var pis project_model.ProjectIssue - has, err := sess.Where("issue_id=?", issue.ID).Get(&pis) - if err != nil { - return err - } - - if !has { - return fmt.Errorf("issue has to be added to a project first") - } - - pis.ProjectBoardID = board.ID - if _, err := sess.ID(pis.ID).Cols("project_board_id").Update(&pis); err != nil { - return err - } - - return committer.Commit() -} diff --git a/models/issue_stopwatch.go b/models/issue_stopwatch.go deleted file mode 100644 index 2cb4a62bd3..0000000000 --- a/models/issue_stopwatch.go +++ /dev/null @@ -1,293 +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 ( - "context" - "fmt" - "time" - - "code.gitea.io/gitea/models/db" - user_model "code.gitea.io/gitea/models/user" - "code.gitea.io/gitea/modules/timeutil" - "code.gitea.io/gitea/modules/util" -) - -// ErrIssueStopwatchNotExist represents an error that stopwatch is not exist -type ErrIssueStopwatchNotExist struct { - UserID int64 - IssueID int64 -} - -func (err ErrIssueStopwatchNotExist) Error() string { - return fmt.Sprintf("issue stopwatch doesn't exist[uid: %d, issue_id: %d", err.UserID, err.IssueID) -} - -// ErrIssueStopwatchAlreadyExist represents an error that stopwatch is already exist -type ErrIssueStopwatchAlreadyExist struct { - UserID int64 - IssueID int64 -} - -func (err ErrIssueStopwatchAlreadyExist) Error() string { - return fmt.Sprintf("issue stopwatch already exists[uid: %d, issue_id: %d", err.UserID, err.IssueID) -} - -// Stopwatch represents a stopwatch for time tracking. -type Stopwatch struct { - ID int64 `xorm:"pk autoincr"` - IssueID int64 `xorm:"INDEX"` - UserID int64 `xorm:"INDEX"` - CreatedUnix timeutil.TimeStamp `xorm:"created"` -} - -func init() { - db.RegisterModel(new(Stopwatch)) -} - -// Seconds returns the amount of time passed since creation, based on local server time -func (s Stopwatch) Seconds() int64 { - return int64(timeutil.TimeStampNow() - s.CreatedUnix) -} - -// Duration returns a human-readable duration string based on local server time -func (s Stopwatch) Duration() string { - return util.SecToTime(s.Seconds()) -} - -func getStopwatch(ctx context.Context, userID, issueID int64) (sw *Stopwatch, exists bool, err error) { - sw = new(Stopwatch) - exists, err = db.GetEngine(ctx). - Where("user_id = ?", userID). - And("issue_id = ?", issueID). - Get(sw) - return -} - -// UserIDCount is a simple coalition of UserID and Count -type UserStopwatch struct { - UserID int64 - StopWatches []*Stopwatch -} - -// GetUIDsAndNotificationCounts between the two provided times -func GetUIDsAndStopwatch() ([]*UserStopwatch, error) { - sws := []*Stopwatch{} - if err := db.GetEngine(db.DefaultContext).Where("issue_id != 0").Find(&sws); err != nil { - return nil, err - } - if len(sws) == 0 { - return []*UserStopwatch{}, nil - } - - lastUserID := int64(-1) - res := []*UserStopwatch{} - for _, sw := range sws { - if lastUserID == sw.UserID { - lastUserStopwatch := res[len(res)-1] - lastUserStopwatch.StopWatches = append(lastUserStopwatch.StopWatches, sw) - } else { - res = append(res, &UserStopwatch{ - UserID: sw.UserID, - StopWatches: []*Stopwatch{sw}, - }) - } - } - return res, nil -} - -// GetUserStopwatches return list of all stopwatches of a user -func GetUserStopwatches(userID int64, listOptions db.ListOptions) ([]*Stopwatch, error) { - sws := make([]*Stopwatch, 0, 8) - sess := db.GetEngine(db.DefaultContext).Where("stopwatch.user_id = ?", userID) - if listOptions.Page != 0 { - sess = db.SetSessionPagination(sess, &listOptions) - } - - err := sess.Find(&sws) - if err != nil { - return nil, err - } - return sws, nil -} - -// CountUserStopwatches return count of all stopwatches of a user -func CountUserStopwatches(userID int64) (int64, error) { - return db.GetEngine(db.DefaultContext).Where("user_id = ?", userID).Count(&Stopwatch{}) -} - -// StopwatchExists returns true if the stopwatch exists -func StopwatchExists(userID, issueID int64) bool { - _, exists, _ := getStopwatch(db.DefaultContext, userID, issueID) - return exists -} - -// HasUserStopwatch returns true if the user has a stopwatch -func HasUserStopwatch(ctx context.Context, userID int64) (exists bool, sw *Stopwatch, err error) { - sw = new(Stopwatch) - exists, err = db.GetEngine(ctx). - Where("user_id = ?", userID). - Get(sw) - return -} - -// FinishIssueStopwatchIfPossible if stopwatch exist then finish it otherwise ignore -func FinishIssueStopwatchIfPossible(ctx context.Context, user *user_model.User, issue *Issue) error { - _, exists, err := getStopwatch(ctx, user.ID, issue.ID) - if err != nil { - return err - } - if !exists { - return nil - } - return FinishIssueStopwatch(ctx, user, issue) -} - -// CreateOrStopIssueStopwatch create an issue stopwatch if it's not exist, otherwise finish it -func CreateOrStopIssueStopwatch(user *user_model.User, issue *Issue) error { - _, exists, err := getStopwatch(db.DefaultContext, user.ID, issue.ID) - if err != nil { - return err - } - if exists { - return FinishIssueStopwatch(db.DefaultContext, user, issue) - } - return CreateIssueStopwatch(db.DefaultContext, user, issue) -} - -// FinishIssueStopwatch if stopwatch exist then finish it otherwise return an error -func FinishIssueStopwatch(ctx context.Context, user *user_model.User, issue *Issue) error { - sw, exists, err := getStopwatch(ctx, user.ID, issue.ID) - if err != nil { - return err - } - if !exists { - return ErrIssueStopwatchNotExist{ - UserID: user.ID, - IssueID: issue.ID, - } - } - - // Create tracked time out of the time difference between start date and actual date - timediff := time.Now().Unix() - int64(sw.CreatedUnix) - - // Create TrackedTime - tt := &TrackedTime{ - Created: time.Now(), - IssueID: issue.ID, - UserID: user.ID, - Time: timediff, - } - - if err := db.Insert(ctx, tt); err != nil { - return err - } - - if err := issue.LoadRepo(ctx); err != nil { - return err - } - - if _, err := CreateCommentCtx(ctx, &CreateCommentOptions{ - Doer: user, - Issue: issue, - Repo: issue.Repo, - Content: util.SecToTime(timediff), - Type: CommentTypeStopTracking, - TimeID: tt.ID, - }); err != nil { - return err - } - _, err = db.DeleteByBean(ctx, sw) - return err -} - -// CreateIssueStopwatch creates a stopwatch if not exist, otherwise return an error -func CreateIssueStopwatch(ctx context.Context, user *user_model.User, issue *Issue) error { - if err := issue.LoadRepo(ctx); err != nil { - return err - } - - // if another stopwatch is running: stop it - exists, sw, err := HasUserStopwatch(ctx, user.ID) - if err != nil { - return err - } - if exists { - issue, err := getIssueByID(ctx, sw.IssueID) - if err != nil { - return err - } - - if err := FinishIssueStopwatch(ctx, user, issue); err != nil { - return err - } - } - - // Create stopwatch - sw = &Stopwatch{ - UserID: user.ID, - IssueID: issue.ID, - } - - if err := db.Insert(ctx, sw); err != nil { - return err - } - - if err := issue.LoadRepo(ctx); err != nil { - return err - } - - if _, err := CreateCommentCtx(ctx, &CreateCommentOptions{ - Doer: user, - Issue: issue, - Repo: issue.Repo, - Type: CommentTypeStartTracking, - }); err != nil { - return err - } - - return nil -} - -// CancelStopwatch removes the given stopwatch and logs it into issue's timeline. -func CancelStopwatch(user *user_model.User, issue *Issue) error { - ctx, committer, err := db.TxContext() - if err != nil { - return err - } - defer committer.Close() - if err := cancelStopwatch(ctx, user, issue); err != nil { - return err - } - return committer.Commit() -} - -func cancelStopwatch(ctx context.Context, user *user_model.User, issue *Issue) error { - e := db.GetEngine(ctx) - sw, exists, err := getStopwatch(ctx, user.ID, issue.ID) - if err != nil { - return err - } - - if exists { - if _, err := e.Delete(sw); err != nil { - return err - } - - if err := issue.LoadRepo(ctx); err != nil { - return err - } - - if _, err := CreateCommentCtx(ctx, &CreateCommentOptions{ - Doer: user, - Issue: issue, - Repo: issue.Repo, - Type: CommentTypeCancelTracking, - }); err != nil { - return err - } - } - return nil -} 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_test.go b/models/issue_test.go deleted file mode 100644 index 5b2f461a84..0000000000 --- a/models/issue_test.go +++ /dev/null @@ -1,614 +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 ( - "context" - "fmt" - "sort" - "strconv" - "sync" - "testing" - "time" - - "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" - 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" - "xorm.io/builder" -) - -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) - 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)) - for i, labelID := range labelIDs { - labels[i] = unittest.AssertExistsAndLoadBean(t, &Label{ID: labelID, RepoID: repo.ID}).(*Label) - } - assert.NoError(t, ReplaceIssueLabels(issue, labels, doer)) - unittest.AssertCount(t, &IssueLabel{IssueID: issueID}, len(labelIDs)) - for _, labelID := range labelIDs { - unittest.AssertExistsAndLoadBean(t, &IssueLabel{IssueID: issueID, LabelID: labelID}) - } - } - - testSuccess(1, []int64{2}) - testSuccess(1, []int64{1, 2}) - testSuccess(1, []int64{}) -} - -func Test_GetIssueIDsByRepoID(t *testing.T) { - assert.NoError(t, unittest.PrepareTestDatabase()) - - ids, err := 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() - - assert.NoError(t, err) - assert.Equal(t, "https://try.gitea.io/api/v1/repos/user2/repo1/issues/1", issue.APIURL()) -} - -func TestGetIssuesByIDs(t *testing.T) { - assert.NoError(t, unittest.PrepareTestDatabase()) - testSuccess := func(expectedIssueIDs, nonExistentIssueIDs []int64) { - issues, err := GetIssuesByIDs(db.DefaultContext, append(expectedIssueIDs, nonExistentIssueIDs...)) - assert.NoError(t, err) - actualIssueIDs := make([]int64, len(issues)) - for i, issue := range issues { - actualIssueIDs[i] = issue.ID - } - assert.Equal(t, expectedIssueIDs, actualIssueIDs) - } - testSuccess([]int64{1, 2, 3}, []int64{}) - testSuccess([]int64{1, 2, 3}, []int64{unittest.NonexistentID}) -} - -func TestGetParticipantIDsByIssue(t *testing.T) { - assert.NoError(t, unittest.PrepareTestDatabase()) - - checkParticipants := func(issueID int64, userIDs []int) { - issue, err := GetIssueByID(issueID) - assert.NoError(t, err) - participants, err := issue.getParticipantIDsByIssue(db.DefaultContext) - if assert.NoError(t, err) { - participantsIDs := make([]int, len(participants)) - for i, uid := range participants { - participantsIDs[i] = int(uid) - } - sort.Ints(participantsIDs) - sort.Ints(userIDs) - assert.Equal(t, userIDs, participantsIDs) - } - } - - // User 1 is issue1 poster (see fixtures/issue.yml) - // User 2 only labeled issue1 (see fixtures/comment.yml) - // Users 3 and 5 made actual comments (see fixtures/comment.yml) - // User 3 is inactive, thus not active participant - checkParticipants(1, []int{1, 5}) -} - -func TestIssue_ClearLabels(t *testing.T) { - tests := []struct { - issueID int64 - doerID int64 - }{ - {1, 2}, // non-pull-request, has labels - {2, 2}, // pull-request, has labels - {3, 2}, // pull-request, has no labels - } - for _, test := range tests { - assert.NoError(t, unittest.PrepareTestDatabase()) - issue := unittest.AssertExistsAndLoadBean(t, &Issue{ID: test.issueID}).(*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}) - } -} - -func TestUpdateIssueCols(t *testing.T) { - assert.NoError(t, unittest.PrepareTestDatabase()) - issue := unittest.AssertExistsAndLoadBean(t, &Issue{}).(*Issue) - - const newTitle = "New Title for unit test" - issue.Title = newTitle - - prevContent := issue.Content - issue.Content = "This should have no effect" - - now := time.Now().Unix() - assert.NoError(t, UpdateIssueCols(db.DefaultContext, issue, "name")) - then := time.Now().Unix() - - updatedIssue := unittest.AssertExistsAndLoadBean(t, &Issue{ID: issue.ID}).(*Issue) - assert.EqualValues(t, newTitle, updatedIssue.Title) - assert.EqualValues(t, prevContent, updatedIssue.Content) - unittest.AssertInt64InRange(t, now, then, int64(updatedIssue.UpdatedUnix)) -} - -func TestIssues(t *testing.T) { - assert.NoError(t, unittest.PrepareTestDatabase()) - for _, test := range []struct { - Opts IssuesOptions - ExpectedIssueIDs []int64 - }{ - { - IssuesOptions{ - AssigneeID: 1, - SortType: "oldest", - }, - []int64{1, 6}, - }, - { - IssuesOptions{ - RepoCond: builder.In("repo_id", 1, 3), - SortType: "oldest", - ListOptions: db.ListOptions{ - Page: 1, - PageSize: 4, - }, - }, - []int64{1, 2, 3, 5}, - }, - { - IssuesOptions{ - LabelIDs: []int64{1}, - ListOptions: db.ListOptions{ - Page: 1, - PageSize: 4, - }, - }, - []int64{2, 1}, - }, - { - IssuesOptions{ - LabelIDs: []int64{1, 2}, - ListOptions: db.ListOptions{ - Page: 1, - PageSize: 4, - }, - }, - []int64{}, // issues with **both** label 1 and 2, none of these issues matches, TODO: add more tests - }, - } { - issues, err := Issues(&test.Opts) - assert.NoError(t, err) - if assert.Len(t, issues, len(test.ExpectedIssueIDs)) { - for i, issue := range issues { - assert.EqualValues(t, test.ExpectedIssueIDs[i], issue.ID) - } - } - } -} - -func TestGetUserIssueStats(t *testing.T) { - assert.NoError(t, unittest.PrepareTestDatabase()) - for _, test := range []struct { - Opts UserIssueStatsOptions - ExpectedIssueStats IssueStats - }{ - { - UserIssueStatsOptions{ - UserID: 1, - RepoIDs: []int64{1}, - FilterMode: FilterModeAll, - }, - IssueStats{ - YourRepositoriesCount: 1, // 6 - AssignCount: 1, // 6 - CreateCount: 1, // 6 - OpenCount: 1, // 6 - ClosedCount: 1, // 1 - }, - }, - { - UserIssueStatsOptions{ - UserID: 1, - RepoIDs: []int64{1}, - FilterMode: FilterModeAll, - IsClosed: true, - }, - IssueStats{ - YourRepositoriesCount: 1, // 6 - AssignCount: 0, - CreateCount: 0, - OpenCount: 1, // 6 - ClosedCount: 1, // 1 - }, - }, - { - UserIssueStatsOptions{ - UserID: 1, - FilterMode: FilterModeAssign, - }, - IssueStats{ - YourRepositoriesCount: 1, // 6 - AssignCount: 1, // 6 - CreateCount: 1, // 6 - OpenCount: 1, // 6 - ClosedCount: 0, - }, - }, - { - UserIssueStatsOptions{ - UserID: 1, - FilterMode: FilterModeCreate, - }, - IssueStats{ - YourRepositoriesCount: 1, // 6 - AssignCount: 1, // 6 - CreateCount: 1, // 6 - OpenCount: 1, // 6 - ClosedCount: 0, - }, - }, - { - UserIssueStatsOptions{ - UserID: 1, - FilterMode: FilterModeMention, - }, - IssueStats{ - YourRepositoriesCount: 1, // 6 - AssignCount: 1, // 6 - CreateCount: 1, // 6 - MentionCount: 0, - OpenCount: 0, - ClosedCount: 0, - }, - }, - { - UserIssueStatsOptions{ - UserID: 1, - FilterMode: FilterModeCreate, - IssueIDs: []int64{1}, - }, - IssueStats{ - YourRepositoriesCount: 1, // 1 - AssignCount: 1, // 1 - CreateCount: 1, // 1 - OpenCount: 1, // 1 - ClosedCount: 0, - }, - }, - { - 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, - }, - IssueStats{ - YourRepositoriesCount: 2, - AssignCount: 1, - CreateCount: 1, - OpenCount: 2, - }, - }, - } { - t.Run(fmt.Sprintf("%#v", test.Opts), func(t *testing.T) { - stats, err := GetUserIssueStats(test.Opts) - if !assert.NoError(t, err) { - return - } - assert.Equal(t, test.ExpectedIssueStats, *stats) - }) - } -} - -func TestIssue_loadTotalTimes(t *testing.T) { - assert.NoError(t, unittest.PrepareTestDatabase()) - ms, err := GetIssueByID(2) - assert.NoError(t, err) - 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) - 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) - 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) - 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) - assert.NoError(t, err) - assert.EqualValues(t, 1, total) - assert.EqualValues(t, []int64{1}, ids) -} - -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 - ExpectedRepoIDs []int64 - }{ - { - IssuesOptions{ - AssigneeID: 2, - }, - []int64{3, 32}, - }, - { - IssuesOptions{ - RepoCond: builder.In("repo_id", 1, 2), - }, - []int64{1, 2}, - }, - } { - repoIDs, err := GetRepoIDsForIssuesOptions(&test.Opts, user) - assert.NoError(t, err) - if assert.Len(t, repoIDs, len(test.ExpectedRepoIDs)) { - for i, repoID := range repoIDs { - assert.EqualValues(t, test.ExpectedRepoIDs[i], repoID) - } - } - } -} - -func testInsertIssue(t *testing.T, title, content string, expectIndex int64) *Issue { - var newIssue 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{ - RepoID: repo.ID, - PosterID: user.ID, - Poster: user, - Title: title, - Content: content, - } - err := NewIssue(repo, &issue, nil, nil) - assert.NoError(t, err) - - has, err := db.GetEngine(db.DefaultContext).ID(issue.ID).Get(&newIssue) - assert.NoError(t, err) - assert.True(t, has) - assert.EqualValues(t, issue.Title, newIssue.Title) - assert.EqualValues(t, issue.Content, newIssue.Content) - if expectIndex > 0 { - assert.EqualValues(t, expectIndex, newIssue.Index) - } - }) - return &newIssue -} - -func TestIssue_InsertIssue(t *testing.T) { - assert.NoError(t, unittest.PrepareTestDatabase()) - - // 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)) - 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)) - 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} - d := unittest.AssertExistsAndLoadBean(t, &user_model.User{LowerName: doer}).(*user_model.User) - resolved, err := ResolveIssueMentionsByVisibility(db.DefaultContext, issue, d, mentions) - assert.NoError(t, err) - ids := make([]int64, len(resolved)) - for i, user := range resolved { - ids[i] = user.ID - } - sort.Slice(ids, func(i, j int) bool { return ids[i] < ids[j] }) - assert.EqualValues(t, expected, ids) - } - - // Public repo, existing user - testSuccess("user2", "repo1", "user1", []string{"user5"}, []int64{5}) - // Public repo, non-existing user - testSuccess("user2", "repo1", "user1", []string{"nonexisting"}, []int64{}) - // Public repo, doer - testSuccess("user2", "repo1", "user1", []string{"user1"}, []int64{}) - // Private repo, team member - testSuccess("user17", "big_test_private_4", "user20", []string{"user2"}, []int64{2}) - // Private repo, not a team member - testSuccess("user17", "big_test_private_4", "user20", []string{"user5"}, []int64{}) - // Private repo, whole team - testSuccess("user17", "big_test_private_4", "user15", []string{"user17/owners"}, []int64{18}) -} - -func TestResourceIndex(t *testing.T) { - assert.NoError(t, unittest.PrepareTestDatabase()) - - var wg sync.WaitGroup - for i := 0; i < 100; i++ { - wg.Add(1) - go func(i int) { - testInsertIssue(t, fmt.Sprintf("issue %d", i+1), "my issue", 0) - wg.Done() - }(i) - } - wg.Wait() -} - -func TestCorrectIssueStats(t *testing.T) { - assert.NoError(t, unittest.PrepareTestDatabase()) - - // Because the condition is to have chunked database look-ups, - // We have to more issues than `maxQueryParameters`, we will insert. - // maxQueryParameters + 10 issues into the testDatabase. - // Each new issues will have a constant description "Bugs are nasty" - // Which will be used later on. - - issueAmount := maxQueryParameters + 10 - - var wg sync.WaitGroup - for i := 0; i < issueAmount; i++ { - wg.Add(1) - go func(i int) { - testInsertIssue(t, fmt.Sprintf("Issue %d", i+1), "Bugs are nasty", 0) - wg.Done() - }(i) - } - 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) - - // Just to be sure. - assert.NoError(t, err) - assert.EqualValues(t, issueAmount, total) - - // Now we will call the GetIssueStats with these IDs and if working, - // get the correct stats back. - issueStats, err := GetIssueStats(&IssueStatsOptions{ - RepoID: 1, - IssueIDs: ids, - }) - - // Now check the values. - assert.NoError(t, err) - assert.EqualValues(t, issueStats.OpenCount, issueAmount) -} - -func TestIssueForeignReference(t *testing.T) { - assert.NoError(t, unittest.PrepareTestDatabase()) - issue := unittest.AssertExistsAndLoadBean(t, &Issue{ID: 4}).(*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() - assert.NoError(t, err) - assert.Nil(t, issue.ForeignReference) - - var foreignIndex int64 = 12345 - _, err = GetIssueByForeignIndex(context.Background(), issue.RepoID, foreignIndex) - assert.True(t, foreignreference.IsErrLocalIndexNotExist(err)) - - _, err = db.GetEngine(db.DefaultContext).Insert(&foreignreference.ForeignReference{ - LocalIndex: issue.Index, - ForeignIndex: strconv.FormatInt(foreignIndex, 10), - RepoID: issue.RepoID, - Type: foreignreference.TypeIssue, - }) - assert.NoError(t, err) - - err = issue.LoadAttributes() - assert.NoError(t, err) - - assert.EqualValues(t, issue.ForeignReference.ForeignIndex, strconv.FormatInt(foreignIndex, 10)) - - found, err := GetIssueByForeignIndex(context.Background(), issue.RepoID, foreignIndex) - assert.NoError(t, err) - assert.EqualValues(t, found.Index, issue.Index) -} - -func TestMilestoneList_LoadTotalTrackedTimes(t *testing.T) { - assert.NoError(t, unittest.PrepareTestDatabase()) - miles := issues_model.MilestoneList{ - unittest.AssertExistsAndLoadBean(t, &issues_model.Milestone{ID: 1}).(*issues_model.Milestone), - } - - assert.NoError(t, miles.LoadTotalTrackedTimes()) - - assert.Equal(t, int64(3682), miles[0].TotalTrackedTime) -} - -func TestLoadTotalTrackedTime(t *testing.T) { - assert.NoError(t, unittest.PrepareTestDatabase()) - milestone := unittest.AssertExistsAndLoadBean(t, &issues_model.Milestone{ID: 1}).(*issues_model.Milestone) - - assert.NoError(t, milestone.LoadTotalTrackedTime()) - - assert.Equal(t, int64(3682), milestone.TotalTrackedTime) -} - -func TestCountIssues(t *testing.T) { - assert.NoError(t, unittest.PrepareTestDatabase()) - count, err := CountIssues(&IssuesOptions{}) - assert.NoError(t, err) - assert.EqualValues(t, 17, count) -} diff --git a/models/issue_tracked_time.go b/models/issue_tracked_time.go deleted file mode 100644 index 30b3905bbc..0000000000 --- a/models/issue_tracked_time.go +++ /dev/null @@ -1,316 +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 ( - "context" - "time" - - "code.gitea.io/gitea/models/db" - user_model "code.gitea.io/gitea/models/user" - "code.gitea.io/gitea/modules/setting" - "code.gitea.io/gitea/modules/util" - - "xorm.io/builder" -) - -// TrackedTime represents a time that was spent for a specific issue. -type TrackedTime struct { - ID int64 `xorm:"pk autoincr"` - IssueID int64 `xorm:"INDEX"` - Issue *Issue `xorm:"-"` - UserID int64 `xorm:"INDEX"` - User *user_model.User `xorm:"-"` - Created time.Time `xorm:"-"` - CreatedUnix int64 `xorm:"created"` - Time int64 `xorm:"NOT NULL"` - Deleted bool `xorm:"NOT NULL DEFAULT false"` -} - -func init() { - db.RegisterModel(new(TrackedTime)) -} - -// TrackedTimeList is a List of TrackedTime's -type TrackedTimeList []*TrackedTime - -// AfterLoad is invoked from XORM after setting the values of all fields of this object. -func (t *TrackedTime) AfterLoad() { - t.Created = time.Unix(t.CreatedUnix, 0).In(setting.DefaultUILocation) -} - -// LoadAttributes load Issue, User -func (t *TrackedTime) LoadAttributes() (err error) { - return t.loadAttributes(db.DefaultContext) -} - -func (t *TrackedTime) loadAttributes(ctx context.Context) (err error) { - if t.Issue == nil { - t.Issue, err = getIssueByID(ctx, t.IssueID) - if err != nil { - return - } - err = t.Issue.LoadRepo(ctx) - if err != nil { - return - } - } - if t.User == nil { - t.User, err = user_model.GetUserByIDCtx(ctx, t.UserID) - if err != nil { - return - } - } - return -} - -// LoadAttributes load Issue, User -func (tl TrackedTimeList) LoadAttributes() (err error) { - for _, t := range tl { - if err = t.LoadAttributes(); err != nil { - return err - } - } - return -} - -// FindTrackedTimesOptions represent the filters for tracked times. If an ID is 0 it will be ignored. -type FindTrackedTimesOptions struct { - db.ListOptions - IssueID int64 - UserID int64 - RepositoryID int64 - MilestoneID int64 - CreatedAfterUnix int64 - CreatedBeforeUnix int64 -} - -// toCond will convert each condition into a xorm-Cond -func (opts *FindTrackedTimesOptions) toCond() builder.Cond { - cond := builder.NewCond().And(builder.Eq{"tracked_time.deleted": false}) - if opts.IssueID != 0 { - cond = cond.And(builder.Eq{"issue_id": opts.IssueID}) - } - if opts.UserID != 0 { - cond = cond.And(builder.Eq{"user_id": opts.UserID}) - } - if opts.RepositoryID != 0 { - cond = cond.And(builder.Eq{"issue.repo_id": opts.RepositoryID}) - } - if opts.MilestoneID != 0 { - cond = cond.And(builder.Eq{"issue.milestone_id": opts.MilestoneID}) - } - if opts.CreatedAfterUnix != 0 { - cond = cond.And(builder.Gte{"tracked_time.created_unix": opts.CreatedAfterUnix}) - } - if opts.CreatedBeforeUnix != 0 { - cond = cond.And(builder.Lte{"tracked_time.created_unix": opts.CreatedBeforeUnix}) - } - return cond -} - -// toSession will convert the given options to a xorm Session by using the conditions from toCond and joining with issue table if required -func (opts *FindTrackedTimesOptions) toSession(e db.Engine) db.Engine { - sess := e - if opts.RepositoryID > 0 || opts.MilestoneID > 0 { - sess = e.Join("INNER", "issue", "issue.id = tracked_time.issue_id") - } - - sess = sess.Where(opts.toCond()) - - if opts.Page != 0 { - sess = db.SetEnginePagination(sess, opts) - } - - return sess -} - -// GetTrackedTimes returns all tracked times that fit to the given options. -func GetTrackedTimes(ctx context.Context, options *FindTrackedTimesOptions) (trackedTimes TrackedTimeList, err error) { - err = options.toSession(db.GetEngine(ctx)).Find(&trackedTimes) - return -} - -// CountTrackedTimes returns count of tracked times that fit to the given options. -func CountTrackedTimes(opts *FindTrackedTimesOptions) (int64, error) { - sess := db.GetEngine(db.DefaultContext).Where(opts.toCond()) - if opts.RepositoryID > 0 || opts.MilestoneID > 0 { - sess = sess.Join("INNER", "issue", "issue.id = tracked_time.issue_id") - } - return sess.Count(&TrackedTime{}) -} - -// GetTrackedSeconds return sum of seconds -func GetTrackedSeconds(ctx context.Context, opts FindTrackedTimesOptions) (trackedSeconds int64, err error) { - return opts.toSession(db.GetEngine(ctx)).SumInt(&TrackedTime{}, "time") -} - -// AddTime will add the given time (in seconds) to the issue -func AddTime(user *user_model.User, issue *Issue, amount int64, created time.Time) (*TrackedTime, error) { - ctx, committer, err := db.TxContext() - if err != nil { - return nil, err - } - defer committer.Close() - - t, err := addTime(ctx, user, issue, amount, created) - if err != nil { - return nil, err - } - - if err := issue.LoadRepo(ctx); err != nil { - return nil, err - } - - if _, err := CreateCommentCtx(ctx, &CreateCommentOptions{ - Issue: issue, - Repo: issue.Repo, - Doer: user, - Content: util.SecToTime(amount), - Type: CommentTypeAddTimeManual, - TimeID: t.ID, - }); err != nil { - return nil, err - } - - return t, committer.Commit() -} - -func addTime(ctx context.Context, user *user_model.User, issue *Issue, amount int64, created time.Time) (*TrackedTime, error) { - if created.IsZero() { - created = time.Now() - } - tt := &TrackedTime{ - IssueID: issue.ID, - UserID: user.ID, - Time: amount, - Created: created, - } - return tt, db.Insert(ctx, tt) -} - -// TotalTimes returns the spent time for each user by an issue -func TotalTimes(options *FindTrackedTimesOptions) (map[*user_model.User]string, error) { - trackedTimes, err := GetTrackedTimes(db.DefaultContext, options) - if err != nil { - return nil, err - } - // Adding total time per user ID - totalTimesByUser := make(map[int64]int64) - for _, t := range trackedTimes { - totalTimesByUser[t.UserID] += t.Time - } - - totalTimes := make(map[*user_model.User]string) - // Fetching User and making time human readable - for userID, total := range totalTimesByUser { - user, err := user_model.GetUserByID(userID) - if err != nil { - if user_model.IsErrUserNotExist(err) { - continue - } - return nil, err - } - totalTimes[user] = util.SecToTime(total) - } - return totalTimes, nil -} - -// DeleteIssueUserTimes deletes times for issue -func DeleteIssueUserTimes(issue *Issue, user *user_model.User) error { - ctx, committer, err := db.TxContext() - if err != nil { - return err - } - defer committer.Close() - - opts := FindTrackedTimesOptions{ - IssueID: issue.ID, - UserID: user.ID, - } - - removedTime, err := deleteTimes(ctx, opts) - if err != nil { - return err - } - if removedTime == 0 { - return db.ErrNotExist{} - } - - if err := issue.LoadRepo(ctx); err != nil { - return err - } - if _, err := CreateCommentCtx(ctx, &CreateCommentOptions{ - Issue: issue, - Repo: issue.Repo, - Doer: user, - Content: "- " + util.SecToTime(removedTime), - Type: CommentTypeDeleteTimeManual, - }); err != nil { - return err - } - - return committer.Commit() -} - -// DeleteTime delete a specific Time -func DeleteTime(t *TrackedTime) error { - ctx, committer, err := db.TxContext() - if err != nil { - return err - } - defer committer.Close() - - if err := t.loadAttributes(ctx); err != nil { - return err - } - - if err := deleteTime(ctx, t); err != nil { - return err - } - - if _, err := CreateCommentCtx(ctx, &CreateCommentOptions{ - Issue: t.Issue, - Repo: t.Issue.Repo, - Doer: t.User, - Content: "- " + util.SecToTime(t.Time), - Type: CommentTypeDeleteTimeManual, - }); err != nil { - return err - } - - return committer.Commit() -} - -func deleteTimes(ctx context.Context, opts FindTrackedTimesOptions) (removedTime int64, err error) { - removedTime, err = GetTrackedSeconds(ctx, opts) - if err != nil || removedTime == 0 { - return - } - - _, err = opts.toSession(db.GetEngine(ctx)).Table("tracked_time").Cols("deleted").Update(&TrackedTime{Deleted: true}) - return -} - -func deleteTime(ctx context.Context, t *TrackedTime) error { - if t.Deleted { - return db.ErrNotExist{ID: t.ID} - } - t.Deleted = true - _, err := db.GetEngine(ctx).ID(t.ID).Cols("deleted").Update(t) - return err -} - -// GetTrackedTimeByID returns raw TrackedTime without loading attributes by id -func GetTrackedTimeByID(id int64) (*TrackedTime, error) { - time := new(TrackedTime) - has, err := db.GetEngine(db.DefaultContext).ID(id).Get(time) - if err != nil { - return nil, err - } else if !has { - return nil, db.ErrNotExist{ID: id} - } - return time, nil -} diff --git a/models/issue_tracked_time_test.go b/models/issue_tracked_time_test.go deleted file mode 100644 index a628329712..0000000000 --- a/models/issue_tracked_time_test.go +++ /dev/null @@ -1,117 +0,0 @@ -// Copyright 2019 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" - "time" - - "code.gitea.io/gitea/models/db" - "code.gitea.io/gitea/models/unittest" - user_model "code.gitea.io/gitea/models/user" - - "github.com/stretchr/testify/assert" -) - -func TestAddTime(t *testing.T) { - assert.NoError(t, unittest.PrepareTestDatabase()) - - user3, err := user_model.GetUserByID(3) - assert.NoError(t, err) - - issue1, err := GetIssueByID(1) - assert.NoError(t, err) - - // 3661 = 1h 1min 1s - trackedTime, err := 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) - assert.Equal(t, int64(3661), tt.Time) - - comment := unittest.AssertExistsAndLoadBean(t, &Comment{Type: CommentTypeAddTimeManual, PosterID: 3, IssueID: 1}).(*Comment) - assert.Equal(t, comment.Content, "1 hour 1 minute") -} - -func TestGetTrackedTimes(t *testing.T) { - assert.NoError(t, unittest.PrepareTestDatabase()) - - // by Issue - times, err := GetTrackedTimes(db.DefaultContext, &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}) - assert.NoError(t, err) - assert.Len(t, times, 0) - - // by User - times, err = GetTrackedTimes(db.DefaultContext, &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}) - assert.NoError(t, err) - assert.Len(t, times, 0) - - // by Repo - times, err = GetTrackedTimes(db.DefaultContext, &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) - assert.NoError(t, err) - assert.Equal(t, issue.RepoID, int64(2)) - - times, err = GetTrackedTimes(db.DefaultContext, &FindTrackedTimesOptions{RepositoryID: 1}) - assert.NoError(t, err) - assert.Len(t, times, 5) - - times, err = GetTrackedTimes(db.DefaultContext, &FindTrackedTimesOptions{RepositoryID: 10}) - assert.NoError(t, err) - assert.Len(t, times, 0) -} - -func TestTotalTimes(t *testing.T) { - assert.NoError(t, unittest.PrepareTestDatabase()) - - total, err := TotalTimes(&FindTrackedTimesOptions{IssueID: 1}) - assert.NoError(t, err) - assert.Len(t, total, 1) - for user, time := range total { - assert.Equal(t, int64(1), user.ID) - assert.Equal(t, "6 minutes 40 seconds", time) - } - - total, err = TotalTimes(&FindTrackedTimesOptions{IssueID: 2}) - assert.NoError(t, err) - assert.Len(t, total, 2) - for user, time := range total { - if user.ID == 2 { - assert.Equal(t, "1 hour 1 minute", time) - } else if user.ID == 1 { - assert.Equal(t, "20 seconds", time) - } else { - assert.Error(t, assert.AnError) - } - } - - total, err = TotalTimes(&FindTrackedTimesOptions{IssueID: 5}) - assert.NoError(t, err) - assert.Len(t, total, 1) - for user, time := range total { - assert.Equal(t, int64(2), user.ID) - assert.Equal(t, "1 second", time) - } - - total, err = TotalTimes(&FindTrackedTimesOptions{IssueID: 4}) - assert.NoError(t, err) - assert.Len(t, total, 2) -} diff --git a/models/issue_user.go b/models/issue_user.go deleted file mode 100644 index 19c64094a1..0000000000 --- a/models/issue_user.go +++ /dev/null @@ -1,86 +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 ( - "context" - "fmt" - - "code.gitea.io/gitea/models/db" - repo_model "code.gitea.io/gitea/models/repo" -) - -// IssueUser represents an issue-user relation. -type IssueUser struct { - ID int64 `xorm:"pk autoincr"` - UID int64 `xorm:"INDEX"` // User ID. - IssueID int64 - IsRead bool - IsMentioned bool -} - -func init() { - db.RegisterModel(new(IssueUser)) -} - -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) - } - - // Poster can be anyone, append later if not one of assignees. - isPosterAssignee := false - - // Leave a seat for poster itself to append later, but if poster is one of assignee - // and just waste 1 unit is cheaper than re-allocate memory once. - issueUsers := make([]*IssueUser, 0, len(assignees)+1) - for _, assignee := range assignees { - issueUsers = append(issueUsers, &IssueUser{ - IssueID: issue.ID, - UID: assignee.ID, - }) - isPosterAssignee = isPosterAssignee || assignee.ID == issue.PosterID - } - if !isPosterAssignee { - issueUsers = append(issueUsers, &IssueUser{ - IssueID: issue.ID, - UID: issue.PosterID, - }) - } - - return db.Insert(ctx, issueUsers) -} - -// UpdateIssueUserByRead updates issue-user relation for reading. -func UpdateIssueUserByRead(uid, issueID int64) error { - _, err := db.GetEngine(db.DefaultContext).Exec("UPDATE `issue_user` SET is_read=? WHERE uid=? AND issue_id=?", true, uid, issueID) - return err -} - -// UpdateIssueUsersByMentions updates issue-user pairs by mentioning. -func UpdateIssueUsersByMentions(ctx context.Context, issueID int64, uids []int64) error { - for _, uid := range uids { - iu := &IssueUser{ - UID: uid, - IssueID: issueID, - } - has, err := db.GetEngine(ctx).Get(iu) - if err != nil { - return err - } - - iu.IsMentioned = true - if has { - _, err = db.GetEngine(ctx).ID(iu.ID).Cols("is_mentioned").Update(iu) - } else { - _, err = db.GetEngine(ctx).Insert(iu) - } - if err != nil { - return err - } - } - return nil -} 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_watch.go b/models/issue_watch.go deleted file mode 100644 index 9f41d36e19..0000000000 --- a/models/issue_watch.go +++ /dev/null @@ -1,134 +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 ( - "context" - - "code.gitea.io/gitea/models/db" - repo_model "code.gitea.io/gitea/models/repo" - user_model "code.gitea.io/gitea/models/user" - "code.gitea.io/gitea/modules/timeutil" -) - -// IssueWatch is connection request for receiving issue notification. -type IssueWatch struct { - ID int64 `xorm:"pk autoincr"` - UserID int64 `xorm:"UNIQUE(watch) NOT NULL"` - IssueID int64 `xorm:"UNIQUE(watch) NOT NULL"` - IsWatching bool `xorm:"NOT NULL"` - CreatedUnix timeutil.TimeStamp `xorm:"created NOT NULL"` - UpdatedUnix timeutil.TimeStamp `xorm:"updated NOT NULL"` -} - -func init() { - db.RegisterModel(new(IssueWatch)) -} - -// IssueWatchList contains IssueWatch -type IssueWatchList []*IssueWatch - -// CreateOrUpdateIssueWatch set watching for a user and issue -func CreateOrUpdateIssueWatch(userID, issueID int64, isWatching bool) error { - iw, exists, err := GetIssueWatch(db.DefaultContext, userID, issueID) - if err != nil { - return err - } - - if !exists { - iw = &IssueWatch{ - UserID: userID, - IssueID: issueID, - IsWatching: isWatching, - } - - if _, err := db.GetEngine(db.DefaultContext).Insert(iw); err != nil { - return err - } - } else { - iw.IsWatching = isWatching - - if _, err := db.GetEngine(db.DefaultContext).ID(iw.ID).Cols("is_watching", "updated_unix").Update(iw); err != nil { - return err - } - } - return nil -} - -// GetIssueWatch returns all IssueWatch objects from db by user and issue -// the current Web-UI need iw object for watchers AND explicit non-watchers -func GetIssueWatch(ctx context.Context, userID, issueID int64) (iw *IssueWatch, exists bool, err error) { - iw = new(IssueWatch) - exists, err = db.GetEngine(ctx). - Where("user_id = ?", userID). - And("issue_id = ?", issueID). - Get(iw) - return -} - -// CheckIssueWatch check if an user is watching an issue -// it takes participants and repo watch into account -func CheckIssueWatch(user *user_model.User, issue *Issue) (bool, error) { - iw, exist, err := GetIssueWatch(db.DefaultContext, user.ID, issue.ID) - if err != nil { - return false, err - } - if exist { - return iw.IsWatching, nil - } - w, err := repo_model.GetWatch(db.DefaultContext, user.ID, issue.RepoID) - if err != nil { - return false, err - } - return repo_model.IsWatchMode(w.Mode) || IsUserParticipantsOfIssue(user, issue), nil -} - -// GetIssueWatchersIDs returns IDs of subscribers or explicit unsubscribers to a given issue id -// but avoids joining with `user` for performance reasons -// User permissions must be verified elsewhere if required -func GetIssueWatchersIDs(ctx context.Context, issueID int64, watching bool) ([]int64, error) { - ids := make([]int64, 0, 64) - return ids, db.GetEngine(ctx).Table("issue_watch"). - Where("issue_id=?", issueID). - And("is_watching = ?", watching). - Select("user_id"). - Find(&ids) -} - -// GetIssueWatchers returns watchers/unwatchers of a given issue -func GetIssueWatchers(ctx context.Context, issueID int64, listOptions db.ListOptions) (IssueWatchList, error) { - sess := db.GetEngine(ctx). - Where("`issue_watch`.issue_id = ?", issueID). - And("`issue_watch`.is_watching = ?", true). - And("`user`.is_active = ?", true). - And("`user`.prohibit_login = ?", false). - Join("INNER", "`user`", "`user`.id = `issue_watch`.user_id") - - if listOptions.Page != 0 { - sess = db.SetSessionPagination(sess, &listOptions) - watches := make([]*IssueWatch, 0, listOptions.PageSize) - return watches, sess.Find(&watches) - } - watches := make([]*IssueWatch, 0, 8) - return watches, sess.Find(&watches) -} - -// CountIssueWatchers count watchers/unwatchers of a given issue -func CountIssueWatchers(ctx context.Context, issueID int64) (int64, error) { - return db.GetEngine(ctx). - Where("`issue_watch`.issue_id = ?", issueID). - And("`issue_watch`.is_watching = ?", true). - And("`user`.is_active = ?", true). - And("`user`.prohibit_login = ?", false). - Join("INNER", "`user`", "`user`.id = `issue_watch`.user_id").Count(new(IssueWatch)) -} - -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). - Delete(new(IssueWatch)) - return err -} diff --git a/models/issue_watch_test.go b/models/issue_watch_test.go deleted file mode 100644 index b686196ae1..0000000000 --- a/models/issue_watch_test.go +++ /dev/null @@ -1,67 +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 ( - "testing" - - "code.gitea.io/gitea/models/db" - "code.gitea.io/gitea/models/unittest" - - "github.com/stretchr/testify/assert" -) - -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.True(t, iw.IsWatching) - - assert.NoError(t, CreateOrUpdateIssueWatch(1, 1, false)) - iw = unittest.AssertExistsAndLoadBean(t, &IssueWatch{UserID: 1, IssueID: 1}).(*IssueWatch) - assert.False(t, iw.IsWatching) -} - -func TestGetIssueWatch(t *testing.T) { - assert.NoError(t, unittest.PrepareTestDatabase()) - - _, exists, err := GetIssueWatch(db.DefaultContext, 9, 1) - assert.True(t, exists) - assert.NoError(t, err) - - iw, exists, err := 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) - assert.False(t, exists) - assert.NoError(t, err) -} - -func TestGetIssueWatchers(t *testing.T) { - assert.NoError(t, unittest.PrepareTestDatabase()) - - iws, err := 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{}) - assert.NoError(t, err) - // Watcher is explicit not watching - assert.Len(t, iws, 0) - - iws, err = 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{}) - assert.NoError(t, err) - // Issue has one watcher - assert.Len(t, iws, 1) -} diff --git a/models/issue_xref.go b/models/issue_xref.go deleted file mode 100644 index 0c1623b5a4..0000000000 --- a/models/issue_xref.go +++ /dev/null @@ -1,371 +0,0 @@ -// Copyright 2019 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" - "fmt" - - "code.gitea.io/gitea/models/db" - access_model "code.gitea.io/gitea/models/perm/access" - repo_model "code.gitea.io/gitea/models/repo" - user_model "code.gitea.io/gitea/models/user" - "code.gitea.io/gitea/modules/log" - "code.gitea.io/gitea/modules/references" -) - -type crossReference struct { - Issue *Issue - Action references.XRefAction -} - -// crossReferencesContext is context to pass along findCrossReference functions -type crossReferencesContext struct { - Type CommentType - Doer *user_model.User - OrigIssue *Issue - OrigComment *Comment - RemoveOld bool -} - -func findOldCrossReferences(ctx context.Context, issueID, commentID int64) ([]*Comment, error) { - active := make([]*Comment, 0, 10) - return active, db.GetEngine(ctx).Where("`ref_action` IN (?, ?, ?)", references.XRefActionNone, references.XRefActionCloses, references.XRefActionReopens). - And("`ref_issue_id` = ?", issueID). - And("`ref_comment_id` = ?", commentID). - Find(&active) -} - -func neuterCrossReferences(ctx context.Context, issueID, commentID int64) error { - active, err := findOldCrossReferences(ctx, issueID, commentID) - if err != nil { - return err - } - ids := make([]int64, len(active)) - for i, c := range active { - ids[i] = c.ID - } - return neuterCrossReferencesIds(ctx, ids) -} - -func neuterCrossReferencesIds(ctx context.Context, ids []int64) error { - _, err := db.GetEngine(ctx).In("id", ids).Cols("`ref_action`").Update(&Comment{RefAction: references.XRefActionNeutered}) - return err -} - -// .___ -// | | ______ ________ __ ____ -// | |/ ___// ___/ | \_/ __ \ -// | |\___ \ \___ \| | /\ ___/ -// |___/____ >____ >____/ \___ > -// \/ \/ \/ -// - -func (issue *Issue) addCrossReferences(stdCtx context.Context, doer *user_model.User, removeOld bool) error { - var commentType CommentType - if issue.IsPull { - commentType = CommentTypePullRef - } else { - commentType = CommentTypeIssueRef - } - ctx := &crossReferencesContext{ - Type: commentType, - Doer: doer, - OrigIssue: issue, - RemoveOld: removeOld, - } - return issue.createCrossReferences(stdCtx, ctx, issue.Title, issue.Content) -} - -func (issue *Issue) createCrossReferences(stdCtx context.Context, ctx *crossReferencesContext, plaincontent, mdcontent string) error { - xreflist, err := ctx.OrigIssue.getCrossReferences(stdCtx, ctx, plaincontent, mdcontent) - if err != nil { - return err - } - if ctx.RemoveOld { - var commentID int64 - if ctx.OrigComment != nil { - commentID = ctx.OrigComment.ID - } - active, err := findOldCrossReferences(stdCtx, ctx.OrigIssue.ID, commentID) - if err != nil { - return err - } - ids := make([]int64, 0, len(active)) - for _, c := range active { - found := false - for i, x := range xreflist { - if x.Issue.ID == c.IssueID && x.Action == c.RefAction { - found = true - xreflist = append(xreflist[:i], xreflist[i+1:]...) - break - } - } - if !found { - ids = append(ids, c.ID) - } - } - if len(ids) > 0 { - if err = neuterCrossReferencesIds(stdCtx, ids); err != nil { - return err - } - } - } - for _, xref := range xreflist { - var refCommentID int64 - if ctx.OrigComment != nil { - refCommentID = ctx.OrigComment.ID - } - opts := &CreateCommentOptions{ - Type: ctx.Type, - Doer: ctx.Doer, - Repo: xref.Issue.Repo, - Issue: xref.Issue, - RefRepoID: ctx.OrigIssue.RepoID, - RefIssueID: ctx.OrigIssue.ID, - RefCommentID: refCommentID, - RefAction: xref.Action, - RefIsPull: ctx.OrigIssue.IsPull, - } - _, err := CreateCommentCtx(stdCtx, opts) - if err != nil { - return err - } - } - return nil -} - -func (issue *Issue) getCrossReferences(stdCtx context.Context, ctx *crossReferencesContext, plaincontent, mdcontent string) ([]*crossReference, error) { - xreflist := make([]*crossReference, 0, 5) - var ( - refRepo *repo_model.Repository - refIssue *Issue - refAction references.XRefAction - err error - ) - - allrefs := append(references.FindAllIssueReferences(plaincontent), references.FindAllIssueReferencesMarkdown(mdcontent)...) - for _, ref := range allrefs { - if ref.Owner == "" && ref.Name == "" { - // Issues in the same repository - if err := ctx.OrigIssue.LoadRepo(stdCtx); err != nil { - return nil, err - } - refRepo = ctx.OrigIssue.Repo - } else { - // Issues in other repositories - refRepo, err = repo_model.GetRepositoryByOwnerAndNameCtx(stdCtx, ref.Owner, ref.Name) - if err != nil { - if repo_model.IsErrRepoNotExist(err) { - continue - } - return nil, err - } - } - if refIssue, refAction, err = ctx.OrigIssue.verifyReferencedIssue(stdCtx, ctx, refRepo, ref); err != nil { - return nil, err - } - if refIssue != nil { - xreflist = ctx.OrigIssue.updateCrossReferenceList(xreflist, &crossReference{ - Issue: refIssue, - Action: refAction, - }) - } - } - - return xreflist, nil -} - -func (issue *Issue) updateCrossReferenceList(list []*crossReference, xref *crossReference) []*crossReference { - if xref.Issue.ID == issue.ID { - return list - } - for i, r := range list { - if r.Issue.ID == xref.Issue.ID { - if xref.Action != references.XRefActionNone { - list[i].Action = xref.Action - } - return list - } - } - return append(list, xref) -} - -// verifyReferencedIssue will check if the referenced issue exists, and whether the doer has permission to do what -func (issue *Issue) verifyReferencedIssue(stdCtx context.Context, ctx *crossReferencesContext, repo *repo_model.Repository, - ref references.IssueReference, -) (*Issue, references.XRefAction, error) { - refIssue := &Issue{RepoID: repo.ID, Index: ref.Index} - refAction := ref.Action - e := db.GetEngine(stdCtx) - - if has, _ := e.Get(refIssue); !has { - return nil, references.XRefActionNone, nil - } - if err := refIssue.LoadRepo(stdCtx); err != nil { - return nil, references.XRefActionNone, err - } - - // Close/reopen actions can only be set from pull requests to issues - if refIssue.IsPull || !issue.IsPull { - refAction = references.XRefActionNone - } - - // Check doer permissions; set action to None if the doer can't change the destination - if refIssue.RepoID != ctx.OrigIssue.RepoID || ref.Action != references.XRefActionNone { - perm, err := access_model.GetUserRepoPermission(stdCtx, refIssue.Repo, ctx.Doer) - if err != nil { - return nil, references.XRefActionNone, err - } - if !perm.CanReadIssuesOrPulls(refIssue.IsPull) { - return nil, references.XRefActionNone, nil - } - // Accept close/reopening actions only if the poster is able to close the - // referenced issue manually at this moment. The only exception is - // the poster of a new PR referencing an issue on the same repo: then the merger - // should be responsible for checking whether the reference should resolve. - if ref.Action != references.XRefActionNone && - ctx.Doer.ID != refIssue.PosterID && - !perm.CanWriteIssuesOrPulls(refIssue.IsPull) && - (refIssue.RepoID != ctx.OrigIssue.RepoID || ctx.OrigComment != nil) { - refAction = references.XRefActionNone - } - } - - return refIssue, refAction, nil -} - -// _________ __ -// \_ ___ \ ____ _____ _____ ____ _____/ |_ -// / \ \/ / _ \ / \ / \_/ __ \ / \ __\ -// \ \___( <_> ) Y Y \ Y Y \ ___/| | \ | -// \______ /\____/|__|_| /__|_| /\___ >___| /__| -// \/ \/ \/ \/ \/ -// - -func (comment *Comment) addCrossReferences(stdCtx context.Context, doer *user_model.User, removeOld bool) error { - if comment.Type != CommentTypeCode && comment.Type != CommentTypeComment { - return nil - } - if err := comment.LoadIssueCtx(stdCtx); err != nil { - return err - } - ctx := &crossReferencesContext{ - Type: CommentTypeCommentRef, - Doer: doer, - OrigIssue: comment.Issue, - OrigComment: comment, - RemoveOld: removeOld, - } - return comment.Issue.createCrossReferences(stdCtx, ctx, "", comment.Content) -} - -func (comment *Comment) neuterCrossReferences(ctx context.Context) error { - return neuterCrossReferences(ctx, comment.IssueID, comment.ID) -} - -// LoadRefComment loads comment that created this reference from database -func (comment *Comment) LoadRefComment() (err error) { - if comment.RefComment != nil { - return nil - } - comment.RefComment, err = GetCommentByID(db.DefaultContext, comment.RefCommentID) - return -} - -// LoadRefIssue loads comment that created this reference from database -func (comment *Comment) LoadRefIssue() (err error) { - if comment.RefIssue != nil { - return nil - } - comment.RefIssue, err = GetIssueByID(comment.RefIssueID) - if err == nil { - err = comment.RefIssue.LoadRepo(db.DefaultContext) - } - return -} - -// CommentTypeIsRef returns true if CommentType is a reference from another issue -func CommentTypeIsRef(t CommentType) bool { - return t == CommentTypeCommentRef || t == CommentTypePullRef || t == CommentTypeIssueRef -} - -// RefCommentHTMLURL returns the HTML URL for the comment that created this reference -func (comment *Comment) RefCommentHTMLURL() string { - // Edge case for when the reference is inside the title or the description of the referring issue - if comment.RefCommentID == 0 { - return comment.RefIssueHTMLURL() - } - if err := comment.LoadRefComment(); err != nil { // Silently dropping errors :unamused: - log.Error("LoadRefComment(%d): %v", comment.RefCommentID, err) - return "" - } - return comment.RefComment.HTMLURL() -} - -// RefIssueHTMLURL returns the HTML URL of the issue where this reference was created -func (comment *Comment) RefIssueHTMLURL() string { - if err := comment.LoadRefIssue(); err != nil { // Silently dropping errors :unamused: - log.Error("LoadRefIssue(%d): %v", comment.RefCommentID, err) - return "" - } - return comment.RefIssue.HTMLURL() -} - -// RefIssueTitle returns the title of the issue where this reference was created -func (comment *Comment) RefIssueTitle() string { - if err := comment.LoadRefIssue(); err != nil { // Silently dropping errors :unamused: - log.Error("LoadRefIssue(%d): %v", comment.RefCommentID, err) - return "" - } - return comment.RefIssue.Title -} - -// RefIssueIdent returns the user friendly identity (e.g. "#1234") of the issue where this reference was created -func (comment *Comment) RefIssueIdent() string { - if err := comment.LoadRefIssue(); err != nil { // Silently dropping errors :unamused: - log.Error("LoadRefIssue(%d): %v", comment.RefCommentID, err) - return "" - } - // FIXME: check this name for cross-repository references (#7901 if it gets merged) - return fmt.Sprintf("#%d", comment.RefIssue.Index) -} - -// __________ .__ .__ __________ __ -// \______ \__ __| | | |\______ \ ____ ________ __ ____ _______/ |_ -// | ___/ | \ | | | | _// __ \/ ____/ | \_/ __ \ / ___/\ __\ -// | | | | / |_| |_| | \ ___< <_| | | /\ ___/ \___ \ | | -// |____| |____/|____/____/____|_ /\___ >__ |____/ \___ >____ > |__| -// \/ \/ |__| \/ \/ - -// ResolveCrossReferences will return the list of references to close/reopen by this PR -func (pr *PullRequest) ResolveCrossReferences(ctx context.Context) ([]*Comment, error) { - unfiltered := make([]*Comment, 0, 5) - if err := db.GetEngine(ctx). - Where("ref_repo_id = ? AND ref_issue_id = ?", pr.Issue.RepoID, pr.Issue.ID). - In("ref_action", []references.XRefAction{references.XRefActionCloses, references.XRefActionReopens}). - OrderBy("id"). - Find(&unfiltered); err != nil { - return nil, fmt.Errorf("get reference: %v", err) - } - - refs := make([]*Comment, 0, len(unfiltered)) - for _, ref := range unfiltered { - found := false - for i, r := range refs { - if r.IssueID == ref.IssueID { - // Keep only the latest - refs[i] = ref - found = true - break - } - } - if !found { - refs = append(refs, ref) - } - } - - return refs, nil -} diff --git a/models/issue_xref_test.go b/models/issue_xref_test.go deleted file mode 100644 index b4ad5b2708..0000000000 --- a/models/issue_xref_test.go +++ /dev/null @@ -1,183 +0,0 @@ -// Copyright 2019 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 ( - "fmt" - "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" - "code.gitea.io/gitea/modules/references" - - "github.com/stretchr/testify/assert" -) - -func TestXRef_AddCrossReferences(t *testing.T) { - assert.NoError(t, unittest.PrepareTestDatabase()) - - // Issue #1 to test against - itarget := testCreateIssue(t, 1, 2, "title1", "content1", false) - - // 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) - assert.Equal(t, pr.RepoID, ref.RefRepoID) - assert.True(t, ref.RefIsPull) - assert.Equal(t, references.XRefActionCloses, ref.RefAction) - - // 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) - assert.Equal(t, pr.RepoID, ref.RefRepoID) - assert.True(t, ref.RefIsPull) - assert.Equal(t, references.XRefActionReopens, ref.RefAction) - - // 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) - assert.Equal(t, pr.RepoID, ref.RefRepoID) - assert.False(t, ref.RefIsPull) - assert.Equal(t, references.XRefActionNone, ref.RefAction) - - // Issue #4 to test against - itarget = testCreateIssue(t, 3, 3, "title4", "content4", false) - - // 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) - assert.Equal(t, i.RepoID, ref.RefRepoID) - assert.False(t, ref.RefIsPull) - assert.Equal(t, references.XRefActionNone, ref.RefAction) - - // 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}) -} - -func TestXRef_NeuterCrossReferences(t *testing.T) { - assert.NoError(t, unittest.PrepareTestDatabase()) - - // Issue #1 to test against - itarget := testCreateIssue(t, 1, 2, "title1", "content1", false) - - // 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) - 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)) - - ref = unittest.AssertExistsAndLoadBean(t, &Comment{IssueID: itarget.ID, RefIssueID: i.ID, RefCommentID: 0}).(*Comment) - assert.Equal(t, CommentTypeIssueRef, ref.Type) - assert.Equal(t, references.XRefActionNeutered, ref.RefAction) -} - -func TestXRef_ResolveCrossReferences(t *testing.T) { - assert.NoError(t, unittest.PrepareTestDatabase()) - - d := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2}).(*user_model.User) - - 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) - 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) - - 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) - - // 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}) - - // 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}) - - 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) - - refs, err := pr.ResolveCrossReferences(db.DefaultContext) - assert.NoError(t, err) - assert.Len(t, refs, 3) - assert.Equal(t, rp.ID, refs[0].ID, "bad ref rp: %+v", refs[0]) - assert.Equal(t, r1.ID, refs[1].ID, "bad ref r1: %+v", refs[1]) - 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 { - 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{ - RepoID: r.ID, - PosterID: d.ID, - Poster: d, - Title: title, - Content: content, - IsPull: ispull, - Index: idx, - } - - ctx, committer, err := db.TxContext() - assert.NoError(t, err) - defer committer.Close() - err = newIssue(ctx, d, NewIssueOptions{ - Repo: r, - Issue: i, - }) - assert.NoError(t, err) - i, err = getIssueByID(ctx, i.ID) - assert.NoError(t, err) - 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 { - 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)) - pr.Issue = i - return pr -} - -func testCreateComment(t *testing.T, repo, doer, issue int64, content string) *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} - - 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, committer.Commit()) - return c -} diff --git a/models/issues/assignees.go b/models/issues/assignees.go new file mode 100644 index 0000000000..5921112fea --- /dev/null +++ b/models/issues/assignees.go @@ -0,0 +1,171 @@ +// Copyright 2018 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 ( + "context" + "fmt" + + "code.gitea.io/gitea/models/db" + user_model "code.gitea.io/gitea/models/user" + "code.gitea.io/gitea/modules/util" +) + +// IssueAssignees saves all issue assignees +type IssueAssignees struct { + ID int64 `xorm:"pk autoincr"` + AssigneeID int64 `xorm:"INDEX"` + IssueID int64 `xorm:"INDEX"` +} + +func init() { + db.RegisterModel(new(IssueAssignees)) +} + +// LoadAssignees load assignees of this issue. +func (issue *Issue) LoadAssignees(ctx context.Context) (err error) { + // Reset maybe preexisting assignees + issue.Assignees = []*user_model.User{} + issue.Assignee = nil + + err = db.GetEngine(ctx).Table("`user`"). + Join("INNER", "issue_assignees", "assignee_id = `user`.id"). + Where("issue_assignees.issue_id = ?", issue.ID). + Find(&issue.Assignees) + if err != nil { + return err + } + + // Check if we have at least one assignee and if yes put it in as `Assignee` + if len(issue.Assignees) > 0 { + issue.Assignee = issue.Assignees[0] + } + return +} + +// GetAssigneeIDsByIssue returns the IDs of users assigned to an issue +// but skips joining with `user` for performance reasons. +// User permissions must be verified elsewhere if required. +func GetAssigneeIDsByIssue(issueID int64) ([]int64, error) { + userIDs := make([]int64, 0, 5) + return userIDs, db.GetEngine(db.DefaultContext).Table("issue_assignees"). + Cols("assignee_id"). + Where("issue_id = ?", issueID). + Distinct("assignee_id"). + Find(&userIDs) +} + +// IsUserAssignedToIssue returns true when the user is assigned to the issue +func IsUserAssignedToIssue(ctx context.Context, issue *Issue, user *user_model.User) (isAssigned bool, err error) { + return db.GetByBean(ctx, &IssueAssignees{IssueID: issue.ID, AssigneeID: user.ID}) +} + +// ToggleIssueAssignee changes a user between assigned and not assigned for this issue, and make issue comment for it. +func ToggleIssueAssignee(issue *Issue, doer *user_model.User, assigneeID int64) (removed bool, comment *Comment, err error) { + ctx, committer, err := db.TxContext() + if err != nil { + return false, nil, err + } + defer committer.Close() + + removed, comment, err = toggleIssueAssignee(ctx, issue, doer, assigneeID, false) + if err != nil { + return false, nil, err + } + + if err := committer.Commit(); err != nil { + return false, nil, err + } + + return removed, comment, nil +} + +func toggleIssueAssignee(ctx context.Context, issue *Issue, doer *user_model.User, assigneeID int64, isCreate bool) (removed bool, comment *Comment, err error) { + removed, err = toggleUserAssignee(ctx, issue, assigneeID) + if err != nil { + return false, nil, fmt.Errorf("UpdateIssueUserByAssignee: %v", err) + } + + // Repo infos + if err = issue.LoadRepo(ctx); err != nil { + return false, nil, fmt.Errorf("loadRepo: %v", err) + } + + opts := &CreateCommentOptions{ + Type: CommentTypeAssignees, + Doer: doer, + Repo: issue.Repo, + Issue: issue, + RemovedAssignee: removed, + AssigneeID: assigneeID, + } + // Comment + comment, err = CreateCommentCtx(ctx, opts) + if err != nil { + return false, nil, fmt.Errorf("createComment: %v", err) + } + + // if pull request is in the middle of creation - don't call webhook + if isCreate { + return removed, comment, err + } + + return removed, comment, nil +} + +// toggles user assignee state in database +func toggleUserAssignee(ctx context.Context, issue *Issue, assigneeID int64) (removed bool, err error) { + // Check if the user exists + assignee, err := user_model.GetUserByIDCtx(ctx, assigneeID) + if err != nil { + return false, err + } + + // Check if the submitted user is already assigned, if yes delete him otherwise add him + found := false + i := 0 + for ; i < len(issue.Assignees); i++ { + if issue.Assignees[i].ID == assigneeID { + found = true + break + } + } + + assigneeIn := IssueAssignees{AssigneeID: assigneeID, IssueID: issue.ID} + if found { + issue.Assignees = append(issue.Assignees[:i], issue.Assignees[i+1:]...) + _, err = db.DeleteByBean(ctx, &assigneeIn) + if err != nil { + return found, err + } + } else { + issue.Assignees = append(issue.Assignees, assignee) + if err = db.Insert(ctx, &assigneeIn); err != nil { + return found, err + } + } + + return found, nil +} + +// MakeIDsFromAPIAssigneesToAdd returns an array with all assignee IDs +func MakeIDsFromAPIAssigneesToAdd(oneAssignee string, multipleAssignees []string) (assigneeIDs []int64, err error) { + var requestAssignees []string + + // Keeping the old assigning method for compatibility reasons + if oneAssignee != "" && !util.IsStringInSlice(oneAssignee, multipleAssignees) { + requestAssignees = append(requestAssignees, oneAssignee) + } + + // Prevent empty assignees + if len(multipleAssignees) > 0 && multipleAssignees[0] != "" { + requestAssignees = append(requestAssignees, multipleAssignees...) + } + + // Get the IDs of all assignees + assigneeIDs, err = user_model.GetUserIDsByNames(requestAssignees, false) + + return +} diff --git a/models/issues/assignees_test.go b/models/issues/assignees_test.go new file mode 100644 index 0000000000..37d966f140 --- /dev/null +++ b/models/issues/assignees_test.go @@ -0,0 +1,92 @@ +// Copyright 2018 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" + + "github.com/stretchr/testify/assert" +) + +func TestUpdateAssignee(t *testing.T) { + assert.NoError(t, unittest.PrepareTestDatabase()) + + // Fake issue with assignees + issue, err := issues_model.GetIssueWithAttrsByID(1) + assert.NoError(t, err) + + // Assign multiple users + user2, err := user_model.GetUserByID(2) + assert.NoError(t, err) + _, _, 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 = 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 = issues_model.ToggleIssueAssignee(issue, &user_model.User{ID: 1}, user1.ID) + assert.NoError(t, err) + + // Check if he got removed + isAssigned, err := issues_model.IsUserAssignedToIssue(db.DefaultContext, issue, user1) + assert.NoError(t, err) + assert.False(t, isAssigned) + + // Check if they're all there + err = issue.LoadAssignees(db.DefaultContext) + assert.NoError(t, err) + + var expectedAssignees []*user_model.User + expectedAssignees = append(expectedAssignees, user2, user3) + + for in, assignee := range issue.Assignees { + assert.Equal(t, assignee.ID, expectedAssignees[in].ID) + } + + // Check if the user is assigned + 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 = issues_model.IsUserAssignedToIssue(db.DefaultContext, issue, &user_model.User{ID: 4}) + assert.NoError(t, err) + assert.False(t, isAssigned) +} + +func TestMakeIDsFromAPIAssigneesToAdd(t *testing.T) { + assert.NoError(t, unittest.PrepareTestDatabase()) + + _ = unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 1}).(*user_model.User) + _ = unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2}).(*user_model.User) + + IDs, err := issues_model.MakeIDsFromAPIAssigneesToAdd("", []string{""}) + assert.NoError(t, err) + assert.Equal(t, []int64{}, IDs) + + _, err = issues_model.MakeIDsFromAPIAssigneesToAdd("", []string{"none_existing_user"}) + assert.Error(t, err) + + IDs, err = issues_model.MakeIDsFromAPIAssigneesToAdd("user1", []string{"user1"}) + assert.NoError(t, err) + assert.Equal(t, []int64{1}, IDs) + + IDs, err = issues_model.MakeIDsFromAPIAssigneesToAdd("user2", []string{""}) + assert.NoError(t, err) + assert.Equal(t, []int64{2}, IDs) + + IDs, err = issues_model.MakeIDsFromAPIAssigneesToAdd("", []string{"user1", "user2"}) + assert.NoError(t, err) + assert.Equal(t, []int64{1, 2}, IDs) +} diff --git a/models/issues/comment.go b/models/issues/comment.go new file mode 100644 index 0000000000..a4e69e7118 --- /dev/null +++ b/models/issues/comment.go @@ -0,0 +1,1546 @@ +// Copyright 2018 The Gitea Authors. +// Copyright 2016 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 + +import ( + "context" + "fmt" + "regexp" + "strconv" + "strings" + "unicode/utf8" + + "code.gitea.io/gitea/models/db" + git_model "code.gitea.io/gitea/models/git" + "code.gitea.io/gitea/models/organization" + project_model "code.gitea.io/gitea/models/project" + repo_model "code.gitea.io/gitea/models/repo" + user_model "code.gitea.io/gitea/models/user" + "code.gitea.io/gitea/modules/git" + "code.gitea.io/gitea/modules/json" + "code.gitea.io/gitea/modules/log" + "code.gitea.io/gitea/modules/markup" + "code.gitea.io/gitea/modules/markup/markdown" + "code.gitea.io/gitea/modules/references" + "code.gitea.io/gitea/modules/structs" + "code.gitea.io/gitea/modules/timeutil" + + "xorm.io/builder" + "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 + +// define unknown comment type +const ( + CommentTypeUnknown CommentType = -1 +) + +// Enumerate all the comment types +const ( + // 0 Plain comment, can be associated with a commit (CommitID > 0) and a line (LineNum > 0) + CommentTypeComment CommentType = iota + CommentTypeReopen // 1 + CommentTypeClose // 2 + + // 3 References. + CommentTypeIssueRef + // 4 Reference from a commit (not part of a pull request) + CommentTypeCommitRef + // 5 Reference from a comment + CommentTypeCommentRef + // 6 Reference from a pull request + CommentTypePullRef + // 7 Labels changed + CommentTypeLabel + // 8 Milestone changed + CommentTypeMilestone + // 9 Assignees changed + CommentTypeAssignees + // 10 Change Title + CommentTypeChangeTitle + // 11 Delete Branch + CommentTypeDeleteBranch + // 12 Start a stopwatch for time tracking + CommentTypeStartTracking + // 13 Stop a stopwatch for time tracking + CommentTypeStopTracking + // 14 Add time manual for time tracking + CommentTypeAddTimeManual + // 15 Cancel a stopwatch for time tracking + CommentTypeCancelTracking + // 16 Added a due date + CommentTypeAddedDeadline + // 17 Modified the due date + CommentTypeModifiedDeadline + // 18 Removed a due date + CommentTypeRemovedDeadline + // 19 Dependency added + CommentTypeAddDependency + // 20 Dependency removed + CommentTypeRemoveDependency + // 21 Comment a line of code + CommentTypeCode + // 22 Reviews a pull request by giving general feedback + CommentTypeReview + // 23 Lock an issue, giving only collaborators access + CommentTypeLock + // 24 Unlocks a previously locked issue + CommentTypeUnlock + // 25 Change pull request's target branch + CommentTypeChangeTargetBranch + // 26 Delete time manual for time tracking + CommentTypeDeleteTimeManual + // 27 add or remove Request from one + CommentTypeReviewRequest + // 28 merge pull request + CommentTypeMergePull + // 29 push to PR head branch + CommentTypePullRequestPush + // 30 Project changed + CommentTypeProject + // 31 Project board changed + CommentTypeProjectBoard + // 32 Dismiss Review + CommentTypeDismissReview + // 33 Change issue ref + CommentTypeChangeIssueRef + // 34 pr was scheduled to auto merge when checks succeed + CommentTypePRScheduledToAutoMerge + // 35 pr was un scheduled to auto merge when checks succeed + CommentTypePRUnScheduledToAutoMerge +) + +var commentStrings = []string{ + "comment", + "reopen", + "close", + "issue_ref", + "commit_ref", + "comment_ref", + "pull_ref", + "label", + "milestone", + "assignees", + "change_title", + "delete_branch", + "start_tracking", + "stop_tracking", + "add_time_manual", + "cancel_tracking", + "added_deadline", + "modified_deadline", + "removed_deadline", + "add_dependency", + "remove_dependency", + "code", + "review", + "lock", + "unlock", + "change_target_branch", + "delete_time_manual", + "review_request", + "merge_pull", + "pull_push", + "project", + "project_board", + "dismiss_review", + "change_issue_ref", + "pull_scheduled_merge", + "pull_cancel_scheduled_merge", +} + +func (t CommentType) String() string { + return commentStrings[t] +} + +// RoleDescriptor defines comment tag type +type RoleDescriptor int + +// Enumerate all the role tags. +const ( + RoleDescriptorNone RoleDescriptor = iota + RoleDescriptorPoster + RoleDescriptorWriter + RoleDescriptorOwner +) + +// WithRole enable a specific tag on the RoleDescriptor. +func (rd RoleDescriptor) WithRole(role RoleDescriptor) RoleDescriptor { + return rd | (1 << role) +} + +func stringToRoleDescriptor(role string) RoleDescriptor { + switch role { + case "Poster": + return RoleDescriptorPoster + case "Writer": + return RoleDescriptorWriter + case "Owner": + return RoleDescriptorOwner + default: + return RoleDescriptorNone + } +} + +// HasRole returns if a certain role is enabled on the RoleDescriptor. +func (rd RoleDescriptor) HasRole(role string) bool { + roleDescriptor := stringToRoleDescriptor(role) + bitValue := rd & (1 << roleDescriptor) + return (bitValue > 0) +} + +// Comment represents a comment in commit and issue page. +type Comment struct { + ID int64 `xorm:"pk autoincr"` + Type CommentType `xorm:"INDEX"` + PosterID int64 `xorm:"INDEX"` + Poster *user_model.User `xorm:"-"` + OriginalAuthor string + OriginalAuthorID int64 + IssueID int64 `xorm:"INDEX"` + Issue *Issue `xorm:"-"` + LabelID int64 + Label *Label `xorm:"-"` + AddedLabels []*Label `xorm:"-"` + RemovedLabels []*Label `xorm:"-"` + OldProjectID int64 + ProjectID int64 + OldProject *project_model.Project `xorm:"-"` + Project *project_model.Project `xorm:"-"` + OldMilestoneID int64 + MilestoneID int64 + OldMilestone *Milestone `xorm:"-"` + Milestone *Milestone `xorm:"-"` + TimeID int64 + Time *TrackedTime `xorm:"-"` + AssigneeID int64 + RemovedAssignee bool + Assignee *user_model.User `xorm:"-"` + AssigneeTeamID int64 `xorm:"NOT NULL DEFAULT 0"` + AssigneeTeam *organization.Team `xorm:"-"` + ResolveDoerID int64 + ResolveDoer *user_model.User `xorm:"-"` + OldTitle string + NewTitle string + OldRef string + NewRef string + DependentIssueID int64 + DependentIssue *Issue `xorm:"-"` + + CommitID int64 + Line int64 // - previous line / + proposed line + TreePath string + Content string `xorm:"LONGTEXT"` + RenderedContent string `xorm:"-"` + + // Path represents the 4 lines of code cemented by this comment + Patch string `xorm:"-"` + PatchQuoted string `xorm:"LONGTEXT patch"` + + CreatedUnix timeutil.TimeStamp `xorm:"INDEX created"` + UpdatedUnix timeutil.TimeStamp `xorm:"INDEX updated"` + + // Reference issue in commit message + CommitSHA string `xorm:"VARCHAR(40)"` + + Attachments []*repo_model.Attachment `xorm:"-"` + Reactions ReactionList `xorm:"-"` + + // For view issue page. + ShowRole RoleDescriptor `xorm:"-"` + + Review *Review `xorm:"-"` + ReviewID int64 `xorm:"index"` + Invalidated bool + + // Reference an issue or pull from another comment, issue or PR + // All information is about the origin of the reference + RefRepoID int64 `xorm:"index"` // Repo where the referencing + RefIssueID int64 `xorm:"index"` + RefCommentID int64 `xorm:"index"` // 0 if origin is Issue title or content (or PR's) + RefAction references.XRefAction `xorm:"SMALLINT"` // What happens if RefIssueID resolves + RefIsPull bool + + RefRepo *repo_model.Repository `xorm:"-"` + RefIssue *Issue `xorm:"-"` + RefComment *Comment `xorm:"-"` + + Commits []*git_model.SignCommitWithStatuses `xorm:"-"` + OldCommit string `xorm:"-"` + NewCommit string `xorm:"-"` + CommitsNum int64 `xorm:"-"` + IsForcePush bool `xorm:"-"` +} + +func init() { + db.RegisterModel(new(Comment)) +} + +// PushActionContent is content of push pull comment +type PushActionContent struct { + IsForcePush bool `json:"is_force_push"` + CommitIDs []string `json:"commit_ids"` +} + +// LoadIssue loads issue from database +func (c *Comment) LoadIssue() (err error) { + return c.LoadIssueCtx(db.DefaultContext) +} + +// LoadIssueCtx loads issue from database +func (c *Comment) LoadIssueCtx(ctx context.Context) (err error) { + if c.Issue != nil { + return nil + } + c.Issue, err = GetIssueByID(ctx, c.IssueID) + return +} + +// BeforeInsert will be invoked by XORM before inserting a record +func (c *Comment) BeforeInsert() { + c.PatchQuoted = c.Patch + if !utf8.ValidString(c.Patch) { + c.PatchQuoted = strconv.Quote(c.Patch) + } +} + +// BeforeUpdate will be invoked by XORM before updating a record +func (c *Comment) BeforeUpdate() { + c.PatchQuoted = c.Patch + if !utf8.ValidString(c.Patch) { + c.PatchQuoted = strconv.Quote(c.Patch) + } +} + +// AfterLoad is invoked from XORM after setting the values of all fields of this object. +func (c *Comment) AfterLoad(session *xorm.Session) { + c.Patch = c.PatchQuoted + if len(c.PatchQuoted) > 0 && c.PatchQuoted[0] == '"' { + unquoted, err := strconv.Unquote(c.PatchQuoted) + if err == nil { + c.Patch = unquoted + } + } +} + +func (c *Comment) loadPoster(ctx context.Context) (err error) { + if c.PosterID <= 0 || c.Poster != nil { + return nil + } + + c.Poster, err = user_model.GetUserByIDCtx(ctx, c.PosterID) + if err != nil { + if user_model.IsErrUserNotExist(err) { + c.PosterID = -1 + c.Poster = user_model.NewGhostUser() + } else { + log.Error("getUserByID[%d]: %v", c.ID, err) + } + } + return err +} + +// AfterDelete is invoked from XORM after the object is deleted. +func (c *Comment) AfterDelete() { + if c.ID <= 0 { + return + } + + _, err := repo_model.DeleteAttachmentsByComment(c.ID, true) + if err != nil { + log.Info("Could not delete files for comment %d on issue #%d: %s", c.ID, c.IssueID, err) + } +} + +// HTMLURL formats a URL-string to the issue-comment +func (c *Comment) HTMLURL() string { + err := c.LoadIssue() + if err != nil { // Silently dropping errors :unamused: + log.Error("LoadIssue(%d): %v", c.IssueID, err) + return "" + } + err = c.Issue.LoadRepo(db.DefaultContext) + if err != nil { // Silently dropping errors :unamused: + log.Error("loadRepo(%d): %v", c.Issue.RepoID, err) + return "" + } + if c.Type == CommentTypeCode { + if c.ReviewID == 0 { + return fmt.Sprintf("%s/files#%s", c.Issue.HTMLURL(), c.HashTag()) + } + if c.Review == nil { + if err := c.LoadReview(); err != nil { + log.Warn("LoadReview(%d): %v", c.ReviewID, err) + return fmt.Sprintf("%s/files#%s", c.Issue.HTMLURL(), c.HashTag()) + } + } + if c.Review.Type <= ReviewTypePending { + return fmt.Sprintf("%s/files#%s", c.Issue.HTMLURL(), c.HashTag()) + } + } + return fmt.Sprintf("%s#%s", c.Issue.HTMLURL(), c.HashTag()) +} + +// APIURL formats a API-string to the issue-comment +func (c *Comment) APIURL() string { + err := c.LoadIssue() + if err != nil { // Silently dropping errors :unamused: + log.Error("LoadIssue(%d): %v", c.IssueID, err) + return "" + } + err = c.Issue.LoadRepo(db.DefaultContext) + if err != nil { // Silently dropping errors :unamused: + log.Error("loadRepo(%d): %v", c.Issue.RepoID, err) + return "" + } + + return fmt.Sprintf("%s/issues/comments/%d", c.Issue.Repo.APIURL(), c.ID) +} + +// IssueURL formats a URL-string to the issue +func (c *Comment) IssueURL() string { + err := c.LoadIssue() + if err != nil { // Silently dropping errors :unamused: + log.Error("LoadIssue(%d): %v", c.IssueID, err) + return "" + } + + if c.Issue.IsPull { + return "" + } + + err = c.Issue.LoadRepo(db.DefaultContext) + if err != nil { // Silently dropping errors :unamused: + log.Error("loadRepo(%d): %v", c.Issue.RepoID, err) + return "" + } + return c.Issue.HTMLURL() +} + +// PRURL formats a URL-string to the pull-request +func (c *Comment) PRURL() string { + err := c.LoadIssue() + if err != nil { // Silently dropping errors :unamused: + log.Error("LoadIssue(%d): %v", c.IssueID, err) + return "" + } + + err = c.Issue.LoadRepo(db.DefaultContext) + if err != nil { // Silently dropping errors :unamused: + log.Error("loadRepo(%d): %v", c.Issue.RepoID, err) + return "" + } + + if !c.Issue.IsPull { + return "" + } + return c.Issue.HTMLURL() +} + +// CommentHashTag returns unique hash tag for comment id. +func CommentHashTag(id int64) string { + return fmt.Sprintf("issuecomment-%d", id) +} + +// HashTag returns unique hash tag for comment. +func (c *Comment) HashTag() string { + return CommentHashTag(c.ID) +} + +// EventTag returns unique event hash tag for comment. +func (c *Comment) EventTag() string { + return fmt.Sprintf("event-%d", c.ID) +} + +// LoadLabel if comment.Type is CommentTypeLabel, then load Label +func (c *Comment) LoadLabel() error { + var label Label + has, err := db.GetEngine(db.DefaultContext).ID(c.LabelID).Get(&label) + if err != nil { + return err + } else if has { + c.Label = &label + } else { + // Ignore Label is deleted, but not clear this table + log.Warn("Commit %d cannot load label %d", c.ID, c.LabelID) + } + + return nil +} + +// LoadProject if comment.Type is CommentTypeProject, then load project. +func (c *Comment) LoadProject() error { + if c.OldProjectID > 0 { + var oldProject project_model.Project + has, err := db.GetEngine(db.DefaultContext).ID(c.OldProjectID).Get(&oldProject) + if err != nil { + return err + } else if has { + c.OldProject = &oldProject + } + } + + if c.ProjectID > 0 { + var project project_model.Project + has, err := db.GetEngine(db.DefaultContext).ID(c.ProjectID).Get(&project) + if err != nil { + return err + } else if has { + c.Project = &project + } + } + + return nil +} + +// LoadMilestone if comment.Type is CommentTypeMilestone, then load milestone +func (c *Comment) LoadMilestone() error { + if c.OldMilestoneID > 0 { + var oldMilestone Milestone + has, err := db.GetEngine(db.DefaultContext).ID(c.OldMilestoneID).Get(&oldMilestone) + if err != nil { + return err + } else if has { + c.OldMilestone = &oldMilestone + } + } + + if c.MilestoneID > 0 { + var milestone Milestone + has, err := db.GetEngine(db.DefaultContext).ID(c.MilestoneID).Get(&milestone) + if err != nil { + return err + } else if has { + c.Milestone = &milestone + } + } + return nil +} + +// LoadPoster loads comment poster +func (c *Comment) LoadPoster() error { + return c.loadPoster(db.DefaultContext) +} + +// LoadAttachments loads attachments (it never returns error, the error during `GetAttachmentsByCommentIDCtx` is ignored) +func (c *Comment) LoadAttachments() error { + if len(c.Attachments) > 0 { + return nil + } + + var err error + c.Attachments, err = repo_model.GetAttachmentsByCommentID(db.DefaultContext, c.ID) + if err != nil { + log.Error("getAttachmentsByCommentID[%d]: %v", c.ID, err) + } + return nil +} + +// UpdateAttachments update attachments by UUIDs for the comment +func (c *Comment) UpdateAttachments(uuids []string) error { + ctx, committer, err := db.TxContext() + if err != nil { + return err + } + defer committer.Close() + + attachments, err := repo_model.GetAttachmentsByUUIDs(ctx, uuids) + if err != nil { + return fmt.Errorf("getAttachmentsByUUIDs [uuids: %v]: %v", uuids, err) + } + for i := 0; i < len(attachments); i++ { + attachments[i].IssueID = c.IssueID + attachments[i].CommentID = c.ID + if err := repo_model.UpdateAttachment(ctx, attachments[i]); err != nil { + return fmt.Errorf("update attachment [id: %d]: %v", attachments[i].ID, err) + } + } + return committer.Commit() +} + +// LoadAssigneeUserAndTeam if comment.Type is CommentTypeAssignees, then load assignees +func (c *Comment) LoadAssigneeUserAndTeam() error { + var err error + + if c.AssigneeID > 0 && c.Assignee == nil { + c.Assignee, err = user_model.GetUserByIDCtx(db.DefaultContext, c.AssigneeID) + if err != nil { + if !user_model.IsErrUserNotExist(err) { + return err + } + c.Assignee = user_model.NewGhostUser() + } + } else if c.AssigneeTeamID > 0 && c.AssigneeTeam == nil { + if err = c.LoadIssue(); err != nil { + return err + } + + if err = c.Issue.LoadRepo(db.DefaultContext); err != nil { + return err + } + + if err = c.Issue.Repo.GetOwner(db.DefaultContext); err != nil { + return err + } + + if c.Issue.Repo.Owner.IsOrganization() { + c.AssigneeTeam, err = organization.GetTeamByID(db.DefaultContext, c.AssigneeTeamID) + if err != nil && !organization.IsErrTeamNotExist(err) { + return err + } + } + } + return nil +} + +// LoadResolveDoer if comment.Type is CommentTypeCode and ResolveDoerID not zero, then load resolveDoer +func (c *Comment) LoadResolveDoer() (err error) { + if c.ResolveDoerID == 0 || c.Type != CommentTypeCode { + return nil + } + c.ResolveDoer, err = user_model.GetUserByIDCtx(db.DefaultContext, c.ResolveDoerID) + if err != nil { + if user_model.IsErrUserNotExist(err) { + c.ResolveDoer = user_model.NewGhostUser() + err = nil + } + } + return +} + +// IsResolved check if an code comment is resolved +func (c *Comment) IsResolved() bool { + return c.ResolveDoerID != 0 && c.Type == CommentTypeCode +} + +// LoadDepIssueDetails loads Dependent Issue Details +func (c *Comment) LoadDepIssueDetails() (err error) { + if c.DependentIssueID <= 0 || c.DependentIssue != nil { + return nil + } + c.DependentIssue, err = GetIssueByID(db.DefaultContext, c.DependentIssueID) + return err +} + +// LoadTime loads the associated time for a CommentTypeAddTimeManual +func (c *Comment) LoadTime() error { + if c.Time != nil || c.TimeID == 0 { + return nil + } + var err error + c.Time, err = GetTrackedTimeByID(c.TimeID) + return err +} + +func (c *Comment) loadReactions(ctx context.Context, repo *repo_model.Repository) (err error) { + if c.Reactions != nil { + return nil + } + c.Reactions, _, err = FindReactions(ctx, FindReactionsOptions{ + IssueID: c.IssueID, + CommentID: c.ID, + }) + if err != nil { + return err + } + // Load reaction user data + if _, err := c.Reactions.LoadUsers(ctx, repo); err != nil { + return err + } + return nil +} + +// LoadReactions loads comment reactions +func (c *Comment) LoadReactions(repo *repo_model.Repository) error { + return c.loadReactions(db.DefaultContext, repo) +} + +func (c *Comment) loadReview(ctx context.Context) (err error) { + if c.Review == nil { + if c.Review, err = GetReviewByID(ctx, c.ReviewID); err != nil { + return err + } + } + c.Review.Issue = c.Issue + return nil +} + +// LoadReview loads the associated review +func (c *Comment) LoadReview() error { + return c.loadReview(db.DefaultContext) +} + +var notEnoughLines = regexp.MustCompile(`fatal: file .* has only \d+ lines?`) + +func (c *Comment) checkInvalidation(doer *user_model.User, repo *git.Repository, branch string) error { + // FIXME differentiate between previous and proposed line + commit, err := repo.LineBlame(branch, repo.Path, c.TreePath, uint(c.UnsignedLine())) + if err != nil && (strings.Contains(err.Error(), "fatal: no such path") || notEnoughLines.MatchString(err.Error())) { + c.Invalidated = true + return UpdateComment(c, doer) + } + if err != nil { + return err + } + if c.CommitSHA != "" && c.CommitSHA != commit.ID.String() { + c.Invalidated = true + return UpdateComment(c, doer) + } + return nil +} + +// CheckInvalidation checks if the line of code comment got changed by another commit. +// If the line got changed the comment is going to be invalidated. +func (c *Comment) CheckInvalidation(repo *git.Repository, doer *user_model.User, branch string) error { + return c.checkInvalidation(doer, repo, branch) +} + +// DiffSide returns "previous" if Comment.Line is a LOC of the previous changes and "proposed" if it is a LOC of the proposed changes. +func (c *Comment) DiffSide() string { + if c.Line < 0 { + return "previous" + } + return "proposed" +} + +// UnsignedLine returns the LOC of the code comment without + or - +func (c *Comment) UnsignedLine() uint64 { + if c.Line < 0 { + return uint64(c.Line * -1) + } + return uint64(c.Line) +} + +// CodeCommentURL returns the url to a comment in code +func (c *Comment) CodeCommentURL() string { + err := c.LoadIssue() + if err != nil { // Silently dropping errors :unamused: + log.Error("LoadIssue(%d): %v", c.IssueID, err) + return "" + } + err = c.Issue.LoadRepo(db.DefaultContext) + if err != nil { // Silently dropping errors :unamused: + log.Error("loadRepo(%d): %v", c.Issue.RepoID, err) + return "" + } + return fmt.Sprintf("%s/files#%s", c.Issue.HTMLURL(), c.HashTag()) +} + +// LoadPushCommits Load push commits +func (c *Comment) LoadPushCommits(ctx context.Context) (err error) { + if c.Content == "" || c.Commits != nil || c.Type != CommentTypePullRequestPush { + return nil + } + + var data PushActionContent + + err = json.Unmarshal([]byte(c.Content), &data) + if err != nil { + return + } + + c.IsForcePush = data.IsForcePush + + if c.IsForcePush { + if len(data.CommitIDs) != 2 { + return nil + } + c.OldCommit = data.CommitIDs[0] + c.NewCommit = data.CommitIDs[1] + } else { + repoPath := c.Issue.Repo.RepoPath() + gitRepo, closer, err := git.RepositoryFromContextOrOpen(ctx, repoPath) + if err != nil { + return err + } + defer closer.Close() + + c.Commits = git_model.ConvertFromGitCommit(gitRepo.GetCommitsFromIDs(data.CommitIDs), c.Issue.Repo) + c.CommitsNum = int64(len(c.Commits)) + } + + return err +} + +// CreateCommentCtx creates comment with context +func CreateCommentCtx(ctx context.Context, opts *CreateCommentOptions) (_ *Comment, err error) { + e := db.GetEngine(ctx) + var LabelID int64 + if opts.Label != nil { + LabelID = opts.Label.ID + } + + comment := &Comment{ + Type: opts.Type, + PosterID: opts.Doer.ID, + Poster: opts.Doer, + IssueID: opts.Issue.ID, + LabelID: LabelID, + OldMilestoneID: opts.OldMilestoneID, + MilestoneID: opts.MilestoneID, + OldProjectID: opts.OldProjectID, + ProjectID: opts.ProjectID, + TimeID: opts.TimeID, + RemovedAssignee: opts.RemovedAssignee, + AssigneeID: opts.AssigneeID, + AssigneeTeamID: opts.AssigneeTeamID, + CommitID: opts.CommitID, + CommitSHA: opts.CommitSHA, + Line: opts.LineNum, + Content: opts.Content, + OldTitle: opts.OldTitle, + NewTitle: opts.NewTitle, + OldRef: opts.OldRef, + NewRef: opts.NewRef, + DependentIssueID: opts.DependentIssueID, + TreePath: opts.TreePath, + ReviewID: opts.ReviewID, + Patch: opts.Patch, + RefRepoID: opts.RefRepoID, + RefIssueID: opts.RefIssueID, + RefCommentID: opts.RefCommentID, + RefAction: opts.RefAction, + RefIsPull: opts.RefIsPull, + IsForcePush: opts.IsForcePush, + Invalidated: opts.Invalidated, + } + if _, err = e.Insert(comment); err != nil { + return nil, err + } + + if err = opts.Repo.GetOwner(ctx); err != nil { + return nil, err + } + + if err = updateCommentInfos(ctx, opts, comment); err != nil { + return nil, err + } + + if err = comment.AddCrossReferences(ctx, opts.Doer, false); err != nil { + return nil, err + } + + return comment, nil +} + +func updateCommentInfos(ctx context.Context, opts *CreateCommentOptions, comment *Comment) (err error) { + // Check comment type. + switch opts.Type { + case CommentTypeCode: + if comment.ReviewID != 0 { + if comment.Review == nil { + if err := comment.loadReview(ctx); err != nil { + return err + } + } + if comment.Review.Type <= ReviewTypePending { + return nil + } + } + fallthrough + case CommentTypeComment: + if _, err = db.Exec(ctx, "UPDATE `issue` SET num_comments=num_comments+1 WHERE id=?", opts.Issue.ID); err != nil { + return err + } + fallthrough + case CommentTypeReview: + // Check attachments + attachments, err := repo_model.GetAttachmentsByUUIDs(ctx, opts.Attachments) + if err != nil { + return fmt.Errorf("getAttachmentsByUUIDs [uuids: %v]: %v", opts.Attachments, err) + } + + for i := range attachments { + attachments[i].IssueID = opts.Issue.ID + attachments[i].CommentID = comment.ID + // No assign value could be 0, so ignore AllCols(). + if _, err = db.GetEngine(ctx).ID(attachments[i].ID).Update(attachments[i]); err != nil { + return fmt.Errorf("update attachment [%d]: %v", attachments[i].ID, err) + } + } + case CommentTypeReopen, CommentTypeClose: + if err = updateIssueClosedNum(ctx, opts.Issue); err != nil { + return err + } + } + // update the issue's updated_unix column + return UpdateIssueCols(ctx, opts.Issue, "updated_unix") +} + +func createDeadlineComment(ctx context.Context, doer *user_model.User, issue *Issue, newDeadlineUnix timeutil.TimeStamp) (*Comment, error) { + var content string + var commentType CommentType + + // newDeadline = 0 means deleting + if newDeadlineUnix == 0 { + commentType = CommentTypeRemovedDeadline + content = issue.DeadlineUnix.Format("2006-01-02") + } else if issue.DeadlineUnix == 0 { + // Check if the new date was added or modified + // If the actual deadline is 0 => deadline added + commentType = CommentTypeAddedDeadline + content = newDeadlineUnix.Format("2006-01-02") + } else { // Otherwise modified + commentType = CommentTypeModifiedDeadline + content = newDeadlineUnix.Format("2006-01-02") + "|" + issue.DeadlineUnix.Format("2006-01-02") + } + + if err := issue.LoadRepo(ctx); err != nil { + return nil, err + } + + opts := &CreateCommentOptions{ + Type: commentType, + Doer: doer, + Repo: issue.Repo, + Issue: issue, + Content: content, + } + comment, err := CreateCommentCtx(ctx, opts) + if err != nil { + return nil, err + } + return comment, nil +} + +// Creates issue dependency comment +func createIssueDependencyComment(ctx context.Context, doer *user_model.User, issue, dependentIssue *Issue, add bool) (err error) { + cType := CommentTypeAddDependency + if !add { + cType = CommentTypeRemoveDependency + } + if err = issue.LoadRepo(ctx); err != nil { + return + } + + // Make two comments, one in each issue + opts := &CreateCommentOptions{ + Type: cType, + Doer: doer, + Repo: issue.Repo, + Issue: issue, + DependentIssueID: dependentIssue.ID, + } + if _, err = CreateCommentCtx(ctx, opts); err != nil { + return + } + + opts = &CreateCommentOptions{ + Type: cType, + Doer: doer, + Repo: issue.Repo, + Issue: dependentIssue, + DependentIssueID: issue.ID, + } + _, err = CreateCommentCtx(ctx, opts) + return +} + +// CreateCommentOptions defines options for creating comment +type CreateCommentOptions struct { + Type CommentType + Doer *user_model.User + Repo *repo_model.Repository + Issue *Issue + Label *Label + + DependentIssueID int64 + OldMilestoneID int64 + MilestoneID int64 + OldProjectID int64 + ProjectID int64 + TimeID int64 + AssigneeID int64 + AssigneeTeamID int64 + RemovedAssignee bool + OldTitle string + NewTitle string + OldRef string + NewRef string + CommitID int64 + CommitSHA string + Patch string + LineNum int64 + TreePath string + ReviewID int64 + Content string + Attachments []string // UUIDs of attachments + RefRepoID int64 + RefIssueID int64 + RefCommentID int64 + RefAction references.XRefAction + RefIsPull bool + IsForcePush bool + Invalidated bool +} + +// CreateComment creates comment of issue or commit. +func CreateComment(opts *CreateCommentOptions) (comment *Comment, err error) { + ctx, committer, err := db.TxContext() + if err != nil { + return nil, err + } + defer committer.Close() + + comment, err = CreateCommentCtx(ctx, opts) + if err != nil { + return nil, err + } + + if err = committer.Commit(); err != nil { + return nil, err + } + + return comment, nil +} + +// CreateRefComment creates a commit reference comment to issue. +func CreateRefComment(doer *user_model.User, repo *repo_model.Repository, issue *Issue, content, commitSHA string) error { + if len(commitSHA) == 0 { + return fmt.Errorf("cannot create reference with empty commit SHA") + } + + // Check if same reference from same commit has already existed. + has, err := db.GetEngine(db.DefaultContext).Get(&Comment{ + Type: CommentTypeCommitRef, + IssueID: issue.ID, + CommitSHA: commitSHA, + }) + if err != nil { + return fmt.Errorf("check reference comment: %v", err) + } else if has { + return nil + } + + _, err = CreateComment(&CreateCommentOptions{ + Type: CommentTypeCommitRef, + Doer: doer, + Repo: repo, + Issue: issue, + CommitSHA: commitSHA, + Content: content, + }) + return err +} + +// GetCommentByID returns the comment by given ID. +func GetCommentByID(ctx context.Context, id int64) (*Comment, error) { + c := new(Comment) + has, err := db.GetEngine(ctx).ID(id).Get(c) + if err != nil { + return nil, err + } else if !has { + return nil, ErrCommentNotExist{id, 0} + } + return c, nil +} + +// FindCommentsOptions describes the conditions to Find comments +type FindCommentsOptions struct { + db.ListOptions + RepoID int64 + IssueID int64 + ReviewID int64 + Since int64 + Before int64 + Line int64 + TreePath string + Type CommentType +} + +func (opts *FindCommentsOptions) toConds() builder.Cond { + cond := builder.NewCond() + if opts.RepoID > 0 { + cond = cond.And(builder.Eq{"issue.repo_id": opts.RepoID}) + } + if opts.IssueID > 0 { + cond = cond.And(builder.Eq{"comment.issue_id": opts.IssueID}) + } + if opts.ReviewID > 0 { + cond = cond.And(builder.Eq{"comment.review_id": opts.ReviewID}) + } + if opts.Since > 0 { + cond = cond.And(builder.Gte{"comment.updated_unix": opts.Since}) + } + if opts.Before > 0 { + cond = cond.And(builder.Lte{"comment.updated_unix": opts.Before}) + } + if opts.Type != CommentTypeUnknown { + cond = cond.And(builder.Eq{"comment.type": opts.Type}) + } + if opts.Line != 0 { + cond = cond.And(builder.Eq{"comment.line": opts.Line}) + } + if len(opts.TreePath) > 0 { + cond = cond.And(builder.Eq{"comment.tree_path": opts.TreePath}) + } + return cond +} + +// FindComments returns all comments according options +func FindComments(ctx context.Context, opts *FindCommentsOptions) ([]*Comment, error) { + comments := make([]*Comment, 0, 10) + sess := db.GetEngine(ctx).Where(opts.toConds()) + if opts.RepoID > 0 { + sess.Join("INNER", "issue", "issue.id = comment.issue_id") + } + + if opts.Page != 0 { + sess = db.SetSessionPagination(sess, opts) + } + + // WARNING: If you change this order you will need to fix createCodeComment + + return comments, sess. + Asc("comment.created_unix"). + Asc("comment.id"). + Find(&comments) +} + +// CountComments count all comments according options by ignoring pagination +func CountComments(opts *FindCommentsOptions) (int64, error) { + sess := db.GetEngine(db.DefaultContext).Where(opts.toConds()) + if opts.RepoID > 0 { + sess.Join("INNER", "issue", "issue.id = comment.issue_id") + } + return sess.Count(&Comment{}) +} + +// UpdateComment updates information of comment. +func UpdateComment(c *Comment, doer *user_model.User) error { + ctx, committer, err := db.TxContext() + if err != nil { + return err + } + defer committer.Close() + sess := db.GetEngine(ctx) + + if _, err := sess.ID(c.ID).AllCols().Update(c); err != nil { + return err + } + if err := c.LoadIssueCtx(ctx); err != nil { + return err + } + if err := c.AddCrossReferences(ctx, doer, true); err != nil { + return err + } + if err := committer.Commit(); err != nil { + return fmt.Errorf("Commit: %v", err) + } + + return nil +} + +// DeleteComment deletes the comment +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, &ContentHistory{ + CommentID: comment.ID, + }); err != nil { + return err + } + + if comment.Type == CommentTypeComment { + if _, err := e.ID(comment.IssueID).Decr("num_comments").Update(new(Issue)); err != nil { + return err + } + } + if _, err := e.Table("action"). + Where("comment_id = ?", comment.ID). + Update(map[string]interface{}{ + "is_deleted": true, + }); err != nil { + return err + } + + if err := comment.neuterCrossReferences(ctx); err != nil { + return err + } + + return DeleteReaction(ctx, &ReactionOptions{CommentID: comment.ID}) +} + +// CodeComments represents comments on code by using this structure: FILENAME -> LINE (+ == proposed; - == previous) -> COMMENTS +type CodeComments map[string]map[int64][]*Comment + +// FetchCodeComments will return a 2d-map: ["Path"]["Line"] = Comments at line +func FetchCodeComments(ctx context.Context, issue *Issue, currentUser *user_model.User) (CodeComments, error) { + return fetchCodeCommentsByReview(ctx, issue, currentUser, nil) +} + +func fetchCodeCommentsByReview(ctx context.Context, issue *Issue, currentUser *user_model.User, review *Review) (CodeComments, error) { + pathToLineToComment := make(CodeComments) + if review == nil { + review = &Review{ID: 0} + } + opts := FindCommentsOptions{ + Type: CommentTypeCode, + IssueID: issue.ID, + ReviewID: review.ID, + } + + comments, err := findCodeComments(ctx, opts, issue, currentUser, review) + if err != nil { + return nil, err + } + + for _, comment := range comments { + if pathToLineToComment[comment.TreePath] == nil { + pathToLineToComment[comment.TreePath] = make(map[int64][]*Comment) + } + pathToLineToComment[comment.TreePath][comment.Line] = append(pathToLineToComment[comment.TreePath][comment.Line], comment) + } + return pathToLineToComment, nil +} + +func findCodeComments(ctx context.Context, opts FindCommentsOptions, issue *Issue, currentUser *user_model.User, review *Review) ([]*Comment, error) { + var comments []*Comment + if review == nil { + review = &Review{ID: 0} + } + conds := opts.toConds() + if review.ID == 0 { + conds = conds.And(builder.Eq{"invalidated": false}) + } + e := db.GetEngine(ctx) + if err := e.Where(conds). + Asc("comment.created_unix"). + Asc("comment.id"). + Find(&comments); err != nil { + return nil, err + } + + if err := issue.LoadRepo(ctx); err != nil { + return nil, err + } + + if err := CommentList(comments).loadPosters(ctx); err != nil { + return nil, err + } + + // Find all reviews by ReviewID + reviews := make(map[int64]*Review) + ids := make([]int64, 0, len(comments)) + for _, comment := range comments { + if comment.ReviewID != 0 { + ids = append(ids, comment.ReviewID) + } + } + if err := e.In("id", ids).Find(&reviews); err != nil { + return nil, err + } + + n := 0 + for _, comment := range comments { + if re, ok := reviews[comment.ReviewID]; ok && re != nil { + // If the review is pending only the author can see the comments (except if the review is set) + if review.ID == 0 && re.Type == ReviewTypePending && + (currentUser == nil || currentUser.ID != re.ReviewerID) { + continue + } + comment.Review = re + } + comments[n] = comment + n++ + + if err := comment.LoadResolveDoer(); err != nil { + return nil, err + } + + if err := comment.LoadReactions(issue.Repo); err != nil { + return nil, err + } + + var err error + if comment.RenderedContent, err = markdown.RenderString(&markup.RenderContext{ + Ctx: ctx, + URLPrefix: issue.Repo.Link(), + Metas: issue.Repo.ComposeMetas(), + }, comment.Content); err != nil { + return nil, err + } + } + return comments[:n], nil +} + +// FetchCodeCommentsByLine fetches the code comments for a given treePath and line number +func FetchCodeCommentsByLine(ctx context.Context, issue *Issue, currentUser *user_model.User, treePath string, line int64) ([]*Comment, error) { + opts := FindCommentsOptions{ + Type: CommentTypeCode, + IssueID: issue.ID, + TreePath: treePath, + Line: line, + } + return findCodeComments(ctx, opts, issue, currentUser, nil) +} + +// UpdateCommentsMigrationsByType updates comments' migrations information via given git service type and original id and poster id +func UpdateCommentsMigrationsByType(tp structs.GitServiceType, originalAuthorID string, posterID int64) error { + _, err := db.GetEngine(db.DefaultContext).Table("comment"). + Where(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, + }), + )). + And("comment.original_author_id = ?", originalAuthorID). + Update(map[string]interface{}{ + "poster_id": posterID, + "original_author": "", + "original_author_id": 0, + }) + return err +} + +// CreatePushPullComment create push code to pull base comment +func CreatePushPullComment(ctx context.Context, pusher *user_model.User, pr *PullRequest, oldCommitID, newCommitID string) (comment *Comment, err error) { + if pr.HasMerged || oldCommitID == "" || newCommitID == "" { + return nil, nil + } + + ops := &CreateCommentOptions{ + Type: CommentTypePullRequestPush, + Doer: pusher, + Repo: pr.BaseRepo, + } + + var data PushActionContent + + data.CommitIDs, data.IsForcePush, err = getCommitIDsFromRepo(ctx, pr.BaseRepo, oldCommitID, newCommitID, pr.BaseBranch) + if err != nil { + return nil, err + } + + ops.Issue = pr.Issue + + dataJSON, err := json.Marshal(data) + if err != nil { + return nil, err + } + + ops.Content = string(dataJSON) + + comment, err = CreateComment(ops) + + return +} + +// CreateAutoMergeComment is a internal function, only use it for CommentTypePRScheduledToAutoMerge and CommentTypePRUnScheduledToAutoMerge CommentTypes +func CreateAutoMergeComment(ctx context.Context, typ CommentType, pr *PullRequest, doer *user_model.User) (comment *Comment, err error) { + if typ != CommentTypePRScheduledToAutoMerge && typ != CommentTypePRUnScheduledToAutoMerge { + return nil, fmt.Errorf("comment type %d cannot be used to create an auto merge comment", typ) + } + if err = pr.LoadIssueCtx(ctx); err != nil { + return + } + + if err = pr.LoadBaseRepoCtx(ctx); err != nil { + return + } + + comment, err = CreateCommentCtx(ctx, &CreateCommentOptions{ + Type: typ, + Doer: doer, + Repo: pr.BaseRepo, + Issue: pr.Issue, + }) + return +} + +// getCommitsFromRepo get commit IDs from repo in between oldCommitID and newCommitID +// isForcePush will be true if oldCommit isn't on the branch +// Commit on baseBranch will skip +func getCommitIDsFromRepo(ctx context.Context, repo *repo_model.Repository, oldCommitID, newCommitID, baseBranch string) (commitIDs []string, isForcePush bool, err error) { + repoPath := repo.RepoPath() + gitRepo, closer, err := git.RepositoryFromContextOrOpen(ctx, repoPath) + if err != nil { + return nil, false, err + } + defer closer.Close() + + oldCommit, err := gitRepo.GetCommit(oldCommitID) + if err != nil { + return nil, false, err + } + + if err = oldCommit.LoadBranchName(); err != nil { + return nil, false, err + } + + if len(oldCommit.Branch) == 0 { + commitIDs = make([]string, 2) + commitIDs[0] = oldCommitID + commitIDs[1] = newCommitID + + return commitIDs, true, err + } + + newCommit, err := gitRepo.GetCommit(newCommitID) + if err != nil { + return nil, false, err + } + + commits, err := newCommit.CommitsBeforeUntil(oldCommitID) + if err != nil { + return nil, false, err + } + + commitIDs = make([]string, 0, len(commits)) + commitChecks := make(map[string]*commitBranchCheckItem) + + for _, commit := range commits { + commitChecks[commit.ID.String()] = &commitBranchCheckItem{ + Commit: commit, + Checked: false, + } + } + + if err = commitBranchCheck(gitRepo, newCommit, oldCommitID, baseBranch, commitChecks); err != nil { + return + } + + for i := len(commits) - 1; i >= 0; i-- { + commitID := commits[i].ID.String() + if item, ok := commitChecks[commitID]; ok && item.Checked { + commitIDs = append(commitIDs, commitID) + } + } + + return +} + +type commitBranchCheckItem struct { + Commit *git.Commit + Checked bool +} + +func commitBranchCheck(gitRepo *git.Repository, startCommit *git.Commit, endCommitID, baseBranch string, commitList map[string]*commitBranchCheckItem) error { + if startCommit.ID.String() == endCommitID { + return nil + } + + checkStack := make([]string, 0, 10) + checkStack = append(checkStack, startCommit.ID.String()) + + for len(checkStack) > 0 { + commitID := checkStack[0] + checkStack = checkStack[1:] + + item, ok := commitList[commitID] + if !ok { + continue + } + + if item.Commit.ID.String() == endCommitID { + continue + } + + if err := item.Commit.LoadBranchName(); err != nil { + return err + } + + if item.Commit.Branch == baseBranch { + continue + } + + if item.Checked { + continue + } + + item.Checked = true + + parentNum := item.Commit.ParentCount() + for i := 0; i < parentNum; i++ { + parentCommit, err := item.Commit.Parent(i) + if err != nil { + return err + } + checkStack = append(checkStack, parentCommit.ID.String()) + } + } + return nil +} + +// RemapExternalUser ExternalUserRemappable interface +func (c *Comment) RemapExternalUser(externalName string, externalID, userID int64) error { + c.OriginalAuthor = externalName + c.OriginalAuthorID = externalID + c.PosterID = userID + return nil +} + +// GetUserID ExternalUserRemappable interface +func (c *Comment) GetUserID() int64 { return c.PosterID } + +// GetExternalName ExternalUserRemappable interface +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/issues/comment_list.go b/models/issues/comment_list.go new file mode 100644 index 0000000000..e3406a5cbe --- /dev/null +++ b/models/issues/comment_list.go @@ -0,0 +1,553 @@ +// Copyright 2018 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 ( + "context" + + "code.gitea.io/gitea/models/db" + repo_model "code.gitea.io/gitea/models/repo" + user_model "code.gitea.io/gitea/models/user" + "code.gitea.io/gitea/modules/container" +) + +// CommentList defines a list of comments +type CommentList []*Comment + +func (comments CommentList) getPosterIDs() []int64 { + posterIDs := make(map[int64]struct{}, len(comments)) + for _, comment := range comments { + if _, ok := posterIDs[comment.PosterID]; !ok { + posterIDs[comment.PosterID] = struct{}{} + } + } + return container.KeysInt64(posterIDs) +} + +func (comments CommentList) loadPosters(ctx context.Context) error { + if len(comments) == 0 { + return nil + } + + posterIDs := comments.getPosterIDs() + posterMaps := make(map[int64]*user_model.User, len(posterIDs)) + left := len(posterIDs) + for left > 0 { + limit := db.DefaultMaxInSize + if left < limit { + limit = left + } + err := db.GetEngine(ctx). + In("id", posterIDs[:limit]). + Find(&posterMaps) + if err != nil { + return err + } + left -= limit + posterIDs = posterIDs[limit:] + } + + for _, comment := range comments { + if comment.PosterID <= 0 { + continue + } + var ok bool + if comment.Poster, ok = posterMaps[comment.PosterID]; !ok { + comment.Poster = user_model.NewGhostUser() + } + } + return nil +} + +func (comments CommentList) getCommentIDs() []int64 { + ids := make([]int64, 0, len(comments)) + for _, comment := range comments { + ids = append(ids, comment.ID) + } + return ids +} + +func (comments CommentList) getLabelIDs() []int64 { + ids := make(map[int64]struct{}, len(comments)) + for _, comment := range comments { + if _, ok := ids[comment.LabelID]; !ok { + ids[comment.LabelID] = struct{}{} + } + } + return container.KeysInt64(ids) +} + +func (comments CommentList) loadLabels(ctx context.Context) error { //nolint + if len(comments) == 0 { + return nil + } + + labelIDs := comments.getLabelIDs() + commentLabels := make(map[int64]*Label, len(labelIDs)) + left := len(labelIDs) + for left > 0 { + limit := db.DefaultMaxInSize + if left < limit { + limit = left + } + rows, err := db.GetEngine(ctx). + In("id", labelIDs[:limit]). + Rows(new(Label)) + if err != nil { + return err + } + + for rows.Next() { + var label Label + err = rows.Scan(&label) + if err != nil { + _ = rows.Close() + return err + } + commentLabels[label.ID] = &label + } + _ = rows.Close() + left -= limit + labelIDs = labelIDs[limit:] + } + + for _, comment := range comments { + comment.Label = commentLabels[comment.ID] + } + return nil +} + +func (comments CommentList) getMilestoneIDs() []int64 { + ids := make(map[int64]struct{}, len(comments)) + for _, comment := range comments { + if _, ok := ids[comment.MilestoneID]; !ok { + ids[comment.MilestoneID] = struct{}{} + } + } + return container.KeysInt64(ids) +} + +func (comments CommentList) loadMilestones(ctx context.Context) error { + if len(comments) == 0 { + return nil + } + + milestoneIDs := comments.getMilestoneIDs() + if len(milestoneIDs) == 0 { + return nil + } + + milestoneMaps := make(map[int64]*Milestone, len(milestoneIDs)) + left := len(milestoneIDs) + for left > 0 { + limit := db.DefaultMaxInSize + if left < limit { + limit = left + } + err := db.GetEngine(ctx). + In("id", milestoneIDs[:limit]). + Find(&milestoneMaps) + if err != nil { + return err + } + left -= limit + milestoneIDs = milestoneIDs[limit:] + } + + for _, issue := range comments { + issue.Milestone = milestoneMaps[issue.MilestoneID] + } + return nil +} + +func (comments CommentList) getOldMilestoneIDs() []int64 { + ids := make(map[int64]struct{}, len(comments)) + for _, comment := range comments { + if _, ok := ids[comment.OldMilestoneID]; !ok { + ids[comment.OldMilestoneID] = struct{}{} + } + } + return container.KeysInt64(ids) +} + +func (comments CommentList) loadOldMilestones(ctx context.Context) error { + if len(comments) == 0 { + return nil + } + + milestoneIDs := comments.getOldMilestoneIDs() + if len(milestoneIDs) == 0 { + return nil + } + + milestoneMaps := make(map[int64]*Milestone, len(milestoneIDs)) + left := len(milestoneIDs) + for left > 0 { + limit := db.DefaultMaxInSize + if left < limit { + limit = left + } + err := db.GetEngine(ctx). + In("id", milestoneIDs[:limit]). + Find(&milestoneMaps) + if err != nil { + return err + } + left -= limit + milestoneIDs = milestoneIDs[limit:] + } + + for _, issue := range comments { + issue.OldMilestone = milestoneMaps[issue.MilestoneID] + } + return nil +} + +func (comments CommentList) getAssigneeIDs() []int64 { + ids := make(map[int64]struct{}, len(comments)) + for _, comment := range comments { + if _, ok := ids[comment.AssigneeID]; !ok { + ids[comment.AssigneeID] = struct{}{} + } + } + return container.KeysInt64(ids) +} + +func (comments CommentList) loadAssignees(ctx context.Context) error { + if len(comments) == 0 { + return nil + } + + assigneeIDs := comments.getAssigneeIDs() + assignees := make(map[int64]*user_model.User, len(assigneeIDs)) + left := len(assigneeIDs) + for left > 0 { + limit := db.DefaultMaxInSize + if left < limit { + limit = left + } + rows, err := db.GetEngine(ctx). + In("id", assigneeIDs[:limit]). + Rows(new(user_model.User)) + if err != nil { + return err + } + + for rows.Next() { + var user user_model.User + err = rows.Scan(&user) + if err != nil { + rows.Close() + return err + } + + assignees[user.ID] = &user + } + _ = rows.Close() + + left -= limit + assigneeIDs = assigneeIDs[limit:] + } + + for _, comment := range comments { + comment.Assignee = assignees[comment.AssigneeID] + } + return nil +} + +// getIssueIDs returns all the issue ids on this comment list which issue hasn't been loaded +func (comments CommentList) getIssueIDs() []int64 { + ids := make(map[int64]struct{}, len(comments)) + for _, comment := range comments { + if comment.Issue != nil { + continue + } + if _, ok := ids[comment.IssueID]; !ok { + ids[comment.IssueID] = struct{}{} + } + } + return container.KeysInt64(ids) +} + +// Issues returns all the issues of comments +func (comments CommentList) Issues() IssueList { + issues := make(map[int64]*Issue, len(comments)) + for _, comment := range comments { + if comment.Issue != nil { + if _, ok := issues[comment.Issue.ID]; !ok { + issues[comment.Issue.ID] = comment.Issue + } + } + } + + issueList := make([]*Issue, 0, len(issues)) + for _, issue := range issues { + issueList = append(issueList, issue) + } + return issueList +} + +func (comments CommentList) loadIssues(ctx context.Context) error { + if len(comments) == 0 { + return nil + } + + issueIDs := comments.getIssueIDs() + issues := make(map[int64]*Issue, len(issueIDs)) + left := len(issueIDs) + for left > 0 { + limit := db.DefaultMaxInSize + if left < limit { + limit = left + } + rows, err := db.GetEngine(ctx). + In("id", issueIDs[:limit]). + Rows(new(Issue)) + if err != nil { + return err + } + + for rows.Next() { + var issue Issue + err = rows.Scan(&issue) + if err != nil { + rows.Close() + return err + } + + issues[issue.ID] = &issue + } + _ = rows.Close() + + left -= limit + issueIDs = issueIDs[limit:] + } + + for _, comment := range comments { + if comment.Issue == nil { + comment.Issue = issues[comment.IssueID] + } + } + return nil +} + +func (comments CommentList) getDependentIssueIDs() []int64 { + ids := make(map[int64]struct{}, len(comments)) + for _, comment := range comments { + if comment.DependentIssue != nil { + continue + } + if _, ok := ids[comment.DependentIssueID]; !ok { + ids[comment.DependentIssueID] = struct{}{} + } + } + return container.KeysInt64(ids) +} + +func (comments CommentList) loadDependentIssues(ctx context.Context) error { + if len(comments) == 0 { + return nil + } + + e := db.GetEngine(ctx) + issueIDs := comments.getDependentIssueIDs() + issues := make(map[int64]*Issue, len(issueIDs)) + left := len(issueIDs) + for left > 0 { + limit := db.DefaultMaxInSize + if left < limit { + limit = left + } + rows, err := e. + In("id", issueIDs[:limit]). + Rows(new(Issue)) + if err != nil { + return err + } + + for rows.Next() { + var issue Issue + err = rows.Scan(&issue) + if err != nil { + _ = rows.Close() + return err + } + + issues[issue.ID] = &issue + } + _ = rows.Close() + + left -= limit + issueIDs = issueIDs[limit:] + } + + for _, comment := range comments { + if comment.DependentIssue == nil { + comment.DependentIssue = issues[comment.DependentIssueID] + if comment.DependentIssue != nil { + if err := comment.DependentIssue.LoadRepo(ctx); err != nil { + return err + } + } + } + } + return nil +} + +func (comments CommentList) loadAttachments(ctx context.Context) (err error) { + if len(comments) == 0 { + return nil + } + + attachments := make(map[int64][]*repo_model.Attachment, len(comments)) + commentsIDs := comments.getCommentIDs() + left := len(commentsIDs) + for left > 0 { + limit := db.DefaultMaxInSize + if left < limit { + limit = left + } + rows, err := db.GetEngine(ctx).Table("attachment"). + Join("INNER", "comment", "comment.id = attachment.comment_id"). + In("comment.id", commentsIDs[:limit]). + Rows(new(repo_model.Attachment)) + if err != nil { + return err + } + + for rows.Next() { + var attachment repo_model.Attachment + err = rows.Scan(&attachment) + if err != nil { + _ = rows.Close() + return err + } + attachments[attachment.CommentID] = append(attachments[attachment.CommentID], &attachment) + } + + _ = rows.Close() + left -= limit + commentsIDs = commentsIDs[limit:] + } + + for _, comment := range comments { + comment.Attachments = attachments[comment.ID] + } + return nil +} + +func (comments CommentList) getReviewIDs() []int64 { + ids := make(map[int64]struct{}, len(comments)) + for _, comment := range comments { + if _, ok := ids[comment.ReviewID]; !ok { + ids[comment.ReviewID] = struct{}{} + } + } + return container.KeysInt64(ids) +} + +func (comments CommentList) loadReviews(ctx context.Context) error { //nolint + if len(comments) == 0 { + return nil + } + + reviewIDs := comments.getReviewIDs() + reviews := make(map[int64]*Review, len(reviewIDs)) + left := len(reviewIDs) + for left > 0 { + limit := db.DefaultMaxInSize + if left < limit { + limit = left + } + rows, err := db.GetEngine(ctx). + In("id", reviewIDs[:limit]). + Rows(new(Review)) + if err != nil { + return err + } + + for rows.Next() { + var review Review + err = rows.Scan(&review) + if err != nil { + _ = rows.Close() + return err + } + + reviews[review.ID] = &review + } + _ = rows.Close() + + left -= limit + reviewIDs = reviewIDs[limit:] + } + + for _, comment := range comments { + comment.Review = reviews[comment.ReviewID] + } + return nil +} + +// loadAttributes loads all attributes +func (comments CommentList) loadAttributes(ctx context.Context) (err error) { + if err = comments.loadPosters(ctx); err != nil { + return + } + + if err = comments.loadLabels(ctx); err != nil { + return + } + + if err = comments.loadMilestones(ctx); err != nil { + return + } + + if err = comments.loadOldMilestones(ctx); err != nil { + return + } + + if err = comments.loadAssignees(ctx); err != nil { + return + } + + if err = comments.loadAttachments(ctx); err != nil { + return + } + + if err = comments.loadReviews(ctx); err != nil { + return + } + + if err = comments.loadIssues(ctx); err != nil { + return + } + + if err = comments.loadDependentIssues(ctx); err != nil { + return + } + + return nil +} + +// LoadAttributes loads attributes of the comments, except for attachments and +// comments +func (comments CommentList) LoadAttributes() error { + return comments.loadAttributes(db.DefaultContext) +} + +// LoadAttachments loads attachments +func (comments CommentList) LoadAttachments() error { + return comments.loadAttachments(db.DefaultContext) +} + +// LoadPosters loads posters +func (comments CommentList) LoadPosters() error { + return comments.loadPosters(db.DefaultContext) +} + +// LoadIssues loads issues of comments +func (comments CommentList) LoadIssues() error { + return comments.loadIssues(db.DefaultContext) +} diff --git a/models/issues/comment_test.go b/models/issues/comment_test.go new file mode 100644 index 0000000000..06b0b85e3c --- /dev/null +++ b/models/issues/comment_test.go @@ -0,0 +1,65 @@ +// 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 ( + "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" + + "github.com/stretchr/testify/assert" +) + +func TestCreateComment(t *testing.T) { + assert.NoError(t, unittest.PrepareTestDatabase()) + + 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 := issues_model.CreateComment(&issues_model.CreateCommentOptions{ + Type: issues_model.CommentTypeComment, + Doer: doer, + Repo: repo, + Issue: issue, + Content: "Hello", + }) + assert.NoError(t, err) + then := time.Now().Unix() + + assert.EqualValues(t, issues_model.CommentTypeComment, comment.Type) + assert.EqualValues(t, "Hello", comment.Content) + assert.EqualValues(t, issue.ID, comment.IssueID) + assert.EqualValues(t, doer.ID, comment.PosterID) + unittest.AssertInt64InRange(t, now, then, int64(comment.CreatedUnix)) + unittest.AssertExistsAndLoadBean(t, comment) // assert actually added to DB + + 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, &issues_model.Issue{ID: 2}).(*issues_model.Issue) + user := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 1}).(*user_model.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)) + assert.Len(t, res["README.md"][4], 1) + 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 = 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/issues/dependency.go b/models/issues/dependency.go new file mode 100644 index 0000000000..d664c0758e --- /dev/null +++ b/models/issues/dependency.go @@ -0,0 +1,210 @@ +// Copyright 2018 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 ( + "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"` + UserID int64 `xorm:"NOT NULL"` + IssueID int64 `xorm:"UNIQUE(issue_dependency) NOT NULL"` + DependencyID int64 `xorm:"UNIQUE(issue_dependency) NOT NULL"` + CreatedUnix timeutil.TimeStamp `xorm:"created"` + UpdatedUnix timeutil.TimeStamp `xorm:"updated"` +} + +func init() { + db.RegisterModel(new(IssueDependency)) +} + +// DependencyType Defines Dependency Type Constants +type DependencyType int + +// Define Dependency Types +const ( + DependencyTypeBlockedBy DependencyType = iota + DependencyTypeBlocking +) + +// CreateIssueDependency creates a new dependency for an issue +func CreateIssueDependency(user *user_model.User, issue, dep *Issue) error { + ctx, committer, err := db.TxContext() + if err != nil { + return err + } + defer committer.Close() + + // Check if it aleready exists + exists, err := issueDepExists(ctx, issue.ID, dep.ID) + if err != nil { + return err + } + if exists { + return ErrDependencyExists{issue.ID, dep.ID} + } + // And if it would be circular + circular, err := issueDepExists(ctx, dep.ID, issue.ID) + if err != nil { + return err + } + if circular { + return ErrCircularDependency{issue.ID, dep.ID} + } + + if err := db.Insert(ctx, &IssueDependency{ + UserID: user.ID, + IssueID: issue.ID, + DependencyID: dep.ID, + }); err != nil { + return err + } + + // Add comment referencing the new dependency + if err = createIssueDependencyComment(ctx, user, issue, dep, true); err != nil { + return err + } + + return committer.Commit() +} + +// RemoveIssueDependency removes a dependency from an issue +func RemoveIssueDependency(user *user_model.User, issue, dep *Issue, depType DependencyType) (err error) { + ctx, committer, err := db.TxContext() + if err != nil { + return err + } + defer committer.Close() + + var issueDepToDelete IssueDependency + + switch depType { + case DependencyTypeBlockedBy: + issueDepToDelete = IssueDependency{IssueID: issue.ID, DependencyID: dep.ID} + case DependencyTypeBlocking: + issueDepToDelete = IssueDependency{IssueID: dep.ID, DependencyID: issue.ID} + default: + return ErrUnknownDependencyType{depType} + } + + affected, err := db.GetEngine(ctx).Delete(&issueDepToDelete) + if err != nil { + return err + } + + // If we deleted nothing, the dependency did not exist + if affected <= 0 { + return ErrDependencyNotExists{issue.ID, dep.ID} + } + + // Add comment referencing the removed dependency + if err = createIssueDependencyComment(ctx, user, issue, dep, false); err != nil { + return err + } + return committer.Commit() +} + +// Check if the dependency already exists +func issueDepExists(ctx context.Context, issueID, depID int64) (bool, error) { + return db.GetEngine(ctx).Where("(issue_id = ? AND dependency_id = ?)", issueID, depID).Exist(&IssueDependency{}) +} + +// IssueNoDependenciesLeft checks if issue can be closed +func IssueNoDependenciesLeft(ctx context.Context, issue *Issue) (bool, error) { + exists, err := db.GetEngine(ctx). + Table("issue_dependency"). + Select("issue.*"). + Join("INNER", "issue", "issue.id = issue_dependency.dependency_id"). + Where("issue_dependency.issue_id = ?", issue.ID). + And("issue.is_closed = ?", "0"). + Exist(&Issue{}) + + return !exists, err +} diff --git a/models/issues/dependency_test.go b/models/issues/dependency_test.go new file mode 100644 index 0000000000..3ea0b4ff5c --- /dev/null +++ b/models/issues/dependency_test.go @@ -0,0 +1,63 @@ +// Copyright 2018 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" + + "github.com/stretchr/testify/assert" +) + +func TestCreateIssueDependency(t *testing.T) { + // Prepare + 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) + + // Create a dependency and check if it was successful + 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 = issues_model.CreateIssueDependency(user1, issue1, issue2) + assert.Error(t, err) + assert.True(t, issues_model.IsErrDependencyExists(err)) + + // Check for circular dependencies + err = issues_model.CreateIssueDependency(user1, issue2, issue1) + assert.Error(t, err) + assert.True(t, issues_model.IsErrCircularDependency(err)) + + _ = unittest.AssertExistsAndLoadBean(t, &issues_model.Comment{Type: issues_model.CommentTypeAddDependency, PosterID: user1.ID, IssueID: issue1.ID}) + + // Check if dependencies left is correct + left, err := issues_model.IssueNoDependenciesLeft(db.DefaultContext, issue1) + assert.NoError(t, err) + assert.False(t, left) + + // Close #2 and check again + _, err = issues_model.ChangeIssueStatus(db.DefaultContext, issue2, user1, true) + assert.NoError(t, err) + + left, err = issues_model.IssueNoDependenciesLeft(db.DefaultContext, issue1) + assert.NoError(t, err) + assert.True(t, left) + + // Test removing the dependency + err = issues_model.RemoveIssueDependency(user1, issue1, issue2, issues_model.DependencyTypeBlockedBy) + assert.NoError(t, err) +} diff --git a/models/issues/issue.go b/models/issues/issue.go new file mode 100644 index 0000000000..0f4af3e84f --- /dev/null +++ b/models/issues/issue.go @@ -0,0 +1,2448 @@ +// Copyright 2014 The Gogs Authors. All rights reserved. +// 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 + +import ( + "context" + "fmt" + "regexp" + "sort" + "strconv" + "strings" + + admin_model "code.gitea.io/gitea/models/admin" + "code.gitea.io/gitea/models/db" + "code.gitea.io/gitea/models/foreignreference" + "code.gitea.io/gitea/models/organization" + "code.gitea.io/gitea/models/perm" + access_model "code.gitea.io/gitea/models/perm/access" + project_model "code.gitea.io/gitea/models/project" + repo_model "code.gitea.io/gitea/models/repo" + "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/git" + "code.gitea.io/gitea/modules/log" + "code.gitea.io/gitea/modules/references" + "code.gitea.io/gitea/modules/setting" + api "code.gitea.io/gitea/modules/structs" + "code.gitea.io/gitea/modules/timeutil" + "code.gitea.io/gitea/modules/util" + + "xorm.io/builder" + "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"` + RepoID int64 `xorm:"INDEX UNIQUE(repo_index)"` + Repo *repo_model.Repository `xorm:"-"` + Index int64 `xorm:"UNIQUE(repo_index)"` // Index in one repository. + 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 *Milestone `xorm:"-"` + Project *project_model.Project `xorm:"-"` + Priority int + AssigneeID int64 `xorm:"-"` + Assignee *user_model.User `xorm:"-"` + IsClosed bool `xorm:"INDEX"` + IsRead bool `xorm:"-"` + IsPull bool `xorm:"INDEX"` // Indicates whether is a pull request or not. + PullRequest *PullRequest `xorm:"-"` + NumComments int + Ref string + + DeadlineUnix timeutil.TimeStamp `xorm:"INDEX"` + + CreatedUnix timeutil.TimeStamp `xorm:"INDEX created"` + UpdatedUnix timeutil.TimeStamp `xorm:"INDEX updated"` + ClosedUnix timeutil.TimeStamp `xorm:"INDEX"` + + Attachments []*repo_model.Attachment `xorm:"-"` + Comments []*Comment `xorm:"-"` + Reactions ReactionList `xorm:"-"` + TotalTrackedTime int64 `xorm:"-"` + Assignees []*user_model.User `xorm:"-"` + ForeignReference *foreignreference.ForeignReference `xorm:"-"` + + // IsLocked limits commenting abilities to users on an issue + // with write access + IsLocked bool `xorm:"NOT NULL DEFAULT false"` + + // For view issue page. + ShowRole RoleDescriptor `xorm:"-"` +} + +var ( + issueTasksPat *regexp.Regexp + issueTasksDonePat *regexp.Regexp +) + +const ( + issueTasksRegexpStr = `(^\s*[-*]\s\[[\sxX]\]\s.)|(\n\s*[-*]\s\[[\sxX]\]\s.)` + issueTasksDoneRegexpStr = `(^\s*[-*]\s\[[xX]\]\s.)|(\n\s*[-*]\s\[[xX]\]\s.)` +) + +// IssueIndex represents the issue index table +type IssueIndex db.ResourceIndex + +func init() { + issueTasksPat = regexp.MustCompile(issueTasksRegexpStr) + issueTasksDonePat = regexp.MustCompile(issueTasksDoneRegexpStr) + + db.RegisterModel(new(Issue)) + db.RegisterModel(new(IssueIndex)) +} + +// 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 { + return err + } + return nil +} + +// IsOverdue checks if the issue is overdue +func (issue *Issue) IsOverdue() bool { + if issue.IsClosed { + return issue.ClosedUnix >= issue.DeadlineUnix + } + return timeutil.TimeStampNow() >= issue.DeadlineUnix +} + +// LoadRepo loads issue's repository +func (issue *Issue) LoadRepo(ctx context.Context) (err error) { + if issue.Repo == nil { + issue.Repo, err = repo_model.GetRepositoryByIDCtx(ctx, issue.RepoID) + if err != nil { + return fmt.Errorf("getRepositoryByID [%d]: %v", issue.RepoID, err) + } + } + return nil +} + +// IsTimetrackerEnabled returns true if the repo enables timetracking +func (issue *Issue) IsTimetrackerEnabled() bool { + return issue.isTimetrackerEnabled(db.DefaultContext) +} + +func (issue *Issue) isTimetrackerEnabled(ctx context.Context) bool { + if err := issue.LoadRepo(ctx); err != nil { + log.Error(fmt.Sprintf("loadRepo: %v", err)) + return false + } + return issue.Repo.IsTimetrackerEnabledCtx(ctx) +} + +// GetPullRequest returns the issue pull request +func (issue *Issue) GetPullRequest() (pr *PullRequest, err error) { + if !issue.IsPull { + return nil, fmt.Errorf("Issue is not a pull request") + } + + pr, err = GetPullRequestByIssueID(db.DefaultContext, issue.ID) + if err != nil { + return nil, err + } + pr.Issue = issue + return +} + +// LoadLabels loads labels +func (issue *Issue) LoadLabels(ctx context.Context) (err error) { + if issue.Labels == nil { + issue.Labels, err = GetLabelsByIssueID(ctx, issue.ID) + if err != nil { + return fmt.Errorf("getLabelsByIssueID [%d]: %v", issue.ID, err) + } + } + return nil +} + +// LoadPoster loads poster +func (issue *Issue) LoadPoster() error { + return issue.loadPoster(db.DefaultContext) +} + +func (issue *Issue) loadPoster(ctx context.Context) (err error) { + if issue.Poster == nil { + issue.Poster, err = user_model.GetUserByIDCtx(ctx, issue.PosterID) + if err != nil { + issue.PosterID = -1 + issue.Poster = user_model.NewGhostUser() + if !user_model.IsErrUserNotExist(err) { + return fmt.Errorf("getUserByID.(poster) [%d]: %v", issue.PosterID, err) + } + err = nil + return + } + } + return +} + +func (issue *Issue) loadPullRequest(ctx context.Context) (err error) { + if issue.IsPull && issue.PullRequest == nil { + issue.PullRequest, err = GetPullRequestByIssueID(ctx, issue.ID) + if err != nil { + if IsErrPullRequestNotExist(err) { + return err + } + return fmt.Errorf("getPullRequestByIssueID [%d]: %v", issue.ID, err) + } + issue.PullRequest.Issue = issue + } + return nil +} + +// LoadPullRequest loads pull request info +func (issue *Issue) LoadPullRequest() error { + return issue.loadPullRequest(db.DefaultContext) +} + +func (issue *Issue) loadComments(ctx context.Context) (err error) { + return issue.loadCommentsByType(ctx, CommentTypeUnknown) +} + +// LoadDiscussComments loads discuss comments +func (issue *Issue) LoadDiscussComments() error { + return issue.loadCommentsByType(db.DefaultContext, CommentTypeComment) +} + +func (issue *Issue) loadCommentsByType(ctx context.Context, tp CommentType) (err error) { + if issue.Comments != nil { + return nil + } + issue.Comments, err = FindComments(ctx, &FindCommentsOptions{ + IssueID: issue.ID, + Type: tp, + }) + return err +} + +func (issue *Issue) loadReactions(ctx context.Context) (err error) { + if issue.Reactions != nil { + return nil + } + reactions, _, err := FindReactions(ctx, FindReactionsOptions{ + IssueID: issue.ID, + }) + if err != nil { + return err + } + if err = issue.LoadRepo(ctx); err != nil { + return err + } + // Load reaction user data + if _, err := ReactionList(reactions).LoadUsers(ctx, issue.Repo); err != nil { + return err + } + + // Cache comments to map + comments := make(map[int64]*Comment) + for _, comment := range issue.Comments { + comments[comment.ID] = comment + } + // Add reactions either to issue or comment + for _, react := range reactions { + if react.CommentID == 0 { + issue.Reactions = append(issue.Reactions, react) + } else if comment, ok := comments[react.CommentID]; ok { + comment.Reactions = append(comment.Reactions, react) + } + } + return nil +} + +func (issue *Issue) loadForeignReference(ctx context.Context) (err error) { + if issue.ForeignReference != nil { + return nil + } + reference := &foreignreference.ForeignReference{ + RepoID: issue.RepoID, + LocalIndex: issue.Index, + Type: foreignreference.TypeIssue, + } + has, err := db.GetEngine(ctx).Get(reference) + if err != nil { + return err + } else if !has { + return foreignreference.ErrForeignIndexNotExist{ + RepoID: issue.RepoID, + LocalIndex: issue.Index, + Type: foreignreference.TypeIssue, + } + } + issue.ForeignReference = reference + return nil +} + +func (issue *Issue) loadMilestone(ctx context.Context) (err error) { + if (issue.Milestone == nil || issue.Milestone.ID != issue.MilestoneID) && issue.MilestoneID > 0 { + 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 +} + +// LoadAttributes loads the attribute of this issue. +func (issue *Issue) LoadAttributes(ctx context.Context) (err error) { + if err = issue.LoadRepo(ctx); err != nil { + return + } + + if err = issue.loadPoster(ctx); err != nil { + return + } + + if err = issue.LoadLabels(ctx); err != nil { + return + } + + if err = issue.loadMilestone(ctx); err != nil { + return + } + + if err = issue.loadProject(ctx); err != nil { + return + } + + if err = issue.LoadAssignees(ctx); err != nil { + return + } + + if err = issue.loadPullRequest(ctx); err != nil && !IsErrPullRequestNotExist(err) { + // It is possible pull request is not yet created. + return err + } + + if issue.Attachments == nil { + issue.Attachments, err = repo_model.GetAttachmentsByIssueID(ctx, issue.ID) + if err != nil { + return fmt.Errorf("getAttachmentsByIssueID [%d]: %v", issue.ID, err) + } + } + + if err = issue.loadComments(ctx); err != nil { + return err + } + + if err = CommentList(issue.Comments).loadAttributes(ctx); err != nil { + return err + } + if issue.isTimetrackerEnabled(ctx) { + if err = issue.LoadTotalTimes(ctx); err != nil { + return err + } + } + + if err = issue.loadForeignReference(ctx); err != nil && !foreignreference.IsErrForeignIndexNotExist(err) { + return err + } + + return issue.loadReactions(ctx) +} + +// LoadMilestone load milestone of this issue. +func (issue *Issue) LoadMilestone() error { + return issue.loadMilestone(db.DefaultContext) +} + +// GetIsRead load the `IsRead` field of the issue +func (issue *Issue) GetIsRead(userID int64) error { + issueUser := &IssueUser{IssueID: issue.ID, UID: userID} + if has, err := db.GetEngine(db.DefaultContext).Get(issueUser); err != nil { + return err + } else if !has { + issue.IsRead = false + return nil + } + issue.IsRead = issueUser.IsRead + return nil +} + +// APIURL returns the absolute APIURL to this issue. +func (issue *Issue) APIURL() string { + if issue.Repo == nil { + err := issue.LoadRepo(db.DefaultContext) + if err != nil { + log.Error("Issue[%d].APIURL(): %v", issue.ID, err) + return "" + } + } + return fmt.Sprintf("%s/issues/%d", issue.Repo.APIURL(), issue.Index) +} + +// HTMLURL returns the absolute URL to this issue. +func (issue *Issue) HTMLURL() string { + var path string + if issue.IsPull { + path = "pulls" + } else { + path = "issues" + } + return fmt.Sprintf("%s/%s/%d", issue.Repo.HTMLURL(), path, issue.Index) +} + +// Link returns the Link URL to this issue. +func (issue *Issue) Link() string { + var path string + if issue.IsPull { + path = "pulls" + } else { + path = "issues" + } + return fmt.Sprintf("%s/%s/%d", issue.Repo.Link(), path, issue.Index) +} + +// DiffURL returns the absolute URL to this diff +func (issue *Issue) DiffURL() string { + if issue.IsPull { + return fmt.Sprintf("%s/pulls/%d.diff", issue.Repo.HTMLURL(), issue.Index) + } + return "" +} + +// PatchURL returns the absolute URL to this patch +func (issue *Issue) PatchURL() string { + if issue.IsPull { + return fmt.Sprintf("%s/pulls/%d.patch", issue.Repo.HTMLURL(), issue.Index) + } + return "" +} + +// State returns string representation of issue status. +func (issue *Issue) State() api.StateType { + if issue.IsClosed { + return api.StateClosed + } + return api.StateOpen +} + +// HashTag returns unique hash tag for issue. +func (issue *Issue) HashTag() string { + return fmt.Sprintf("issue-%d", issue.ID) +} + +// IsPoster returns true if given user by ID is the poster. +func (issue *Issue) IsPoster(uid int64) bool { + return issue.OriginalAuthorID == 0 && issue.PosterID == uid +} + +func (issue *Issue) getLabels(ctx context.Context) (err error) { + if len(issue.Labels) > 0 { + return nil + } + + issue.Labels, err = GetLabelsByIssueID(ctx, issue.ID) + if err != nil { + return fmt.Errorf("getLabelsByIssueID: %v", err) + } + return nil +} + +func clearIssueLabels(ctx context.Context, issue *Issue, doer *user_model.User) (err error) { + if err = issue.getLabels(ctx); err != nil { + return fmt.Errorf("getLabels: %v", err) + } + + for i := range issue.Labels { + if err = deleteIssueLabel(ctx, issue, issue.Labels[i], doer); err != nil { + return fmt.Errorf("removeLabel: %v", err) + } + } + + return nil +} + +// ClearIssueLabels removes all issue labels as the given user. +// Triggers appropriate WebHooks, if any. +func ClearIssueLabels(issue *Issue, doer *user_model.User) (err error) { + ctx, committer, err := db.TxContext() + if err != nil { + return err + } + defer committer.Close() + + if err := issue.LoadRepo(ctx); err != nil { + return err + } else if err = issue.loadPullRequest(ctx); err != nil { + return err + } + + perm, err := access_model.GetUserRepoPermission(ctx, issue.Repo, doer) + if err != nil { + return err + } + if !perm.CanWriteIssuesOrPulls(issue.IsPull) { + return ErrRepoLabelNotExist{} + } + + if err = clearIssueLabels(ctx, issue, doer); err != nil { + return err + } + + if err = committer.Commit(); err != nil { + return fmt.Errorf("Commit: %v", err) + } + + return nil +} + +type labelSorter []*Label + +func (ts labelSorter) Len() int { + return len([]*Label(ts)) +} + +func (ts labelSorter) Less(i, j int) bool { + return []*Label(ts)[i].ID < []*Label(ts)[j].ID +} + +func (ts labelSorter) Swap(i, j int) { + []*Label(ts)[i], []*Label(ts)[j] = []*Label(ts)[j], []*Label(ts)[i] +} + +// ReplaceIssueLabels removes all current labels and add new labels to the issue. +// Triggers appropriate WebHooks, if any. +func ReplaceIssueLabels(issue *Issue, labels []*Label, doer *user_model.User) (err error) { + ctx, committer, err := db.TxContext() + if err != nil { + return err + } + defer committer.Close() + + if err = issue.LoadRepo(ctx); err != nil { + return err + } + + if err = issue.LoadLabels(ctx); err != nil { + return err + } + + sort.Sort(labelSorter(labels)) + sort.Sort(labelSorter(issue.Labels)) + + var toAdd, toRemove []*Label + + addIndex, removeIndex := 0, 0 + for addIndex < len(labels) && removeIndex < len(issue.Labels) { + addLabel := labels[addIndex] + removeLabel := issue.Labels[removeIndex] + if addLabel.ID == removeLabel.ID { + // Silently drop invalid labels + if removeLabel.RepoID != issue.RepoID && removeLabel.OrgID != issue.Repo.OwnerID { + toRemove = append(toRemove, removeLabel) + } + + addIndex++ + removeIndex++ + } else if addLabel.ID < removeLabel.ID { + // Only add if the label is valid + if addLabel.RepoID == issue.RepoID || addLabel.OrgID == issue.Repo.OwnerID { + toAdd = append(toAdd, addLabel) + } + addIndex++ + } else { + toRemove = append(toRemove, removeLabel) + removeIndex++ + } + } + toAdd = append(toAdd, labels[addIndex:]...) + toRemove = append(toRemove, issue.Labels[removeIndex:]...) + + if len(toAdd) > 0 { + if err = newIssueLabels(ctx, issue, toAdd, doer); err != nil { + return fmt.Errorf("addLabels: %v", err) + } + } + + for _, l := range toRemove { + if err = deleteIssueLabel(ctx, issue, l, doer); err != nil { + return fmt.Errorf("removeLabel: %v", err) + } + } + + issue.Labels = nil + if err = issue.LoadLabels(ctx); err != nil { + return err + } + + return committer.Commit() +} + +// 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 { + return err + } + return nil +} + +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) + if err != nil { + return nil, err + } + + // Nothing should be performed if current status is same as target status + if currentIssue.IsClosed == isClosed { + if !issue.IsPull { + return nil, ErrIssueWasClosed{ + ID: issue.ID, + } + } + return nil, ErrPullWasClosed{ + ID: issue.ID, + } + } + + issue.IsClosed = isClosed + return doChangeIssueStatus(ctx, issue, doer, isMergePull) +} + +func doChangeIssueStatus(ctx context.Context, issue *Issue, doer *user_model.User, isMergePull bool) (*Comment, error) { + // Check for open dependencies + if issue.IsClosed && issue.Repo.IsDependenciesEnabledCtx(ctx) { + // only check if dependencies are enabled and we're about to close an issue, otherwise reopening an issue would fail when there are unsatisfied dependencies + noDeps, err := IssueNoDependenciesLeft(ctx, issue) + if err != nil { + return nil, err + } + + if !noDeps { + return nil, ErrDependenciesLeft{issue.ID} + } + } + + if issue.IsClosed { + issue.ClosedUnix = timeutil.TimeStampNow() + } else { + issue.ClosedUnix = 0 + } + + if err := UpdateIssueCols(ctx, issue, "is_closed", "closed_unix"); err != nil { + return nil, err + } + + // Update issue count of labels + if err := issue.getLabels(ctx); err != nil { + return nil, err + } + for idx := range issue.Labels { + if err := updateLabelCols(ctx, issue.Labels[idx], "num_issues", "num_closed_issue"); err != nil { + return nil, err + } + } + + // Update issue count of milestone + if issue.MilestoneID > 0 { + if err := UpdateMilestoneCounters(ctx, issue.MilestoneID); err != nil { + return nil, err + } + } + + if err := updateIssueClosedNum(ctx, issue); err != nil { + return nil, err + } + + // New action comment + cmtType := CommentTypeClose + if !issue.IsClosed { + cmtType = CommentTypeReopen + } else if isMergePull { + cmtType = CommentTypeMergePull + } + + return CreateCommentCtx(ctx, &CreateCommentOptions{ + Type: cmtType, + Doer: doer, + Repo: issue.Repo, + Issue: issue, + }) +} + +// ChangeIssueStatus changes issue status to open or closed. +func ChangeIssueStatus(ctx context.Context, issue *Issue, doer *user_model.User, isClosed bool) (*Comment, error) { + if err := issue.LoadRepo(ctx); err != nil { + return nil, err + } + if err := issue.loadPoster(ctx); err != nil { + return nil, err + } + + return changeIssueStatus(ctx, issue, doer, isClosed, false) +} + +// ChangeIssueTitle changes the title of this issue, as the given user. +func ChangeIssueTitle(issue *Issue, doer *user_model.User, oldTitle string) (err error) { + ctx, committer, err := db.TxContext() + if err != nil { + return err + } + defer committer.Close() + + if err = UpdateIssueCols(ctx, issue, "name"); err != nil { + return fmt.Errorf("updateIssueCols: %v", err) + } + + if err = issue.LoadRepo(ctx); err != nil { + return fmt.Errorf("loadRepo: %v", err) + } + + opts := &CreateCommentOptions{ + Type: CommentTypeChangeTitle, + Doer: doer, + Repo: issue.Repo, + Issue: issue, + OldTitle: oldTitle, + NewTitle: issue.Title, + } + if _, err = CreateCommentCtx(ctx, opts); err != nil { + return fmt.Errorf("createComment: %v", err) + } + if err = issue.AddCrossReferences(ctx, doer, true); err != nil { + return err + } + + return committer.Commit() +} + +// ChangeIssueRef changes the branch of this issue, as the given user. +func ChangeIssueRef(issue *Issue, doer *user_model.User, oldRef string) (err error) { + ctx, committer, err := db.TxContext() + if err != nil { + return err + } + defer committer.Close() + + if err = UpdateIssueCols(ctx, issue, "ref"); err != nil { + return fmt.Errorf("updateIssueCols: %v", err) + } + + if err = issue.LoadRepo(ctx); err != nil { + return fmt.Errorf("loadRepo: %v", err) + } + oldRefFriendly := strings.TrimPrefix(oldRef, git.BranchPrefix) + newRefFriendly := strings.TrimPrefix(issue.Ref, git.BranchPrefix) + + opts := &CreateCommentOptions{ + Type: CommentTypeChangeIssueRef, + Doer: doer, + Repo: issue.Repo, + Issue: issue, + OldRef: oldRefFriendly, + NewRef: newRefFriendly, + } + if _, err = CreateCommentCtx(ctx, opts); err != nil { + return fmt.Errorf("createComment: %v", err) + } + + return committer.Commit() +} + +// 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) + if err != nil { + return err + } + opts := &CreateCommentOptions{ + Type: CommentTypeDeleteBranch, + Doer: doer, + Repo: repo, + Issue: issue, + OldRef: branchName, + } + _, err = CreateCommentCtx(ctx, opts) + return err +} + +// UpdateIssueAttachments update attachments by UUIDs for the issue +func UpdateIssueAttachments(issueID int64, uuids []string) (err error) { + ctx, committer, err := db.TxContext() + if err != nil { + return err + } + defer committer.Close() + attachments, err := repo_model.GetAttachmentsByUUIDs(ctx, uuids) + if err != nil { + return fmt.Errorf("getAttachmentsByUUIDs [uuids: %v]: %v", uuids, err) + } + for i := 0; i < len(attachments); i++ { + attachments[i].IssueID = issueID + if err := repo_model.UpdateAttachment(ctx, attachments[i]); err != nil { + return fmt.Errorf("update attachment [id: %d]: %v", attachments[i].ID, err) + } + } + return committer.Commit() +} + +// ChangeIssueContent changes issue content, as the given user. +func ChangeIssueContent(issue *Issue, doer *user_model.User, content string) (err error) { + ctx, committer, err := db.TxContext() + if err != nil { + return err + } + defer committer.Close() + + hasContentHistory, err := HasIssueContentHistory(ctx, issue.ID, 0) + if err != nil { + return fmt.Errorf("HasIssueContentHistory: %v", err) + } + if !hasContentHistory { + if err = SaveIssueContentHistory(ctx, issue.PosterID, issue.ID, 0, + issue.CreatedUnix, issue.Content, true); err != nil { + return fmt.Errorf("SaveIssueContentHistory: %v", err) + } + } + + issue.Content = content + + if err = UpdateIssueCols(ctx, issue, "content"); err != nil { + return fmt.Errorf("UpdateIssueCols: %v", err) + } + + 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 { + return fmt.Errorf("addCrossReferences: %v", err) + } + + return committer.Commit() +} + +// GetTasks returns the amount of tasks in the issues content +func (issue *Issue) GetTasks() int { + return len(issueTasksPat.FindAllStringIndex(issue.Content, -1)) +} + +// GetTasksDone returns the amount of completed tasks in the issues content +func (issue *Issue) GetTasksDone() int { + return len(issueTasksDonePat.FindAllStringIndex(issue.Content, -1)) +} + +// GetLastEventTimestamp returns the last user visible event timestamp, either the creation of this issue or the close. +func (issue *Issue) GetLastEventTimestamp() timeutil.TimeStamp { + if issue.IsClosed { + return issue.ClosedUnix + } + return issue.CreatedUnix +} + +// GetLastEventLabel returns the localization label for the current issue. +func (issue *Issue) GetLastEventLabel() string { + if issue.IsClosed { + if issue.IsPull && issue.PullRequest.HasMerged { + return "repo.pulls.merged_by" + } + return "repo.issues.closed_by" + } + return "repo.issues.opened_by" +} + +// GetLastComment return last comment for the current issue. +func (issue *Issue) GetLastComment() (*Comment, error) { + var c Comment + exist, err := db.GetEngine(db.DefaultContext).Where("type = ?", CommentTypeComment). + And("issue_id = ?", issue.ID).Desc("created_unix").Get(&c) + if err != nil { + return nil, err + } + if !exist { + return nil, nil + } + return &c, nil +} + +// GetLastEventLabelFake returns the localization label for the current issue without providing a link in the username. +func (issue *Issue) GetLastEventLabelFake() string { + if issue.IsClosed { + if issue.IsPull && issue.PullRequest.HasMerged { + return "repo.pulls.merged_by_fake" + } + return "repo.issues.closed_by_fake" + } + return "repo.issues.opened_by_fake" +} + +// NewIssueOptions represents the options of a new issue. +type NewIssueOptions struct { + Repo *repo_model.Repository + Issue *Issue + LabelIDs []int64 + Attachments []string // In UUID format. + IsPull bool +} + +// 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 := GetMilestoneByRepoID(ctx, opts.Issue.RepoID, opts.Issue.MilestoneID) + if err != nil && !IsErrMilestoneNotExist(err) { + return fmt.Errorf("getMilestoneByID: %v", err) + } + + // Assume milestone is invalid and drop silently. + opts.Issue.MilestoneID = 0 + if milestone != nil { + opts.Issue.MilestoneID = milestone.ID + opts.Issue.Milestone = milestone + } + } + + if opts.Issue.Index <= 0 { + return fmt.Errorf("no issue index provided") + } + if opts.Issue.ID > 0 { + return fmt.Errorf("issue exist") + } + + if _, err := e.Insert(opts.Issue); err != nil { + return err + } + + if opts.Issue.MilestoneID > 0 { + if err := UpdateMilestoneCounters(ctx, opts.Issue.MilestoneID); err != nil { + return err + } + + opts := &CreateCommentOptions{ + Type: CommentTypeMilestone, + Doer: doer, + Repo: opts.Repo, + Issue: opts.Issue, + OldMilestoneID: 0, + MilestoneID: opts.Issue.MilestoneID, + } + if _, err = CreateCommentCtx(ctx, opts); err != nil { + return err + } + } + + if opts.IsPull { + _, err = e.Exec("UPDATE `repository` SET num_pulls = num_pulls + 1 WHERE id = ?", opts.Issue.RepoID) + } else { + _, err = e.Exec("UPDATE `repository` SET num_issues = num_issues + 1 WHERE id = ?", opts.Issue.RepoID) + } + if err != nil { + return err + } + + if len(opts.LabelIDs) > 0 { + // During the session, SQLite3 driver cannot handle retrieve objects after update something. + // So we have to get all needed labels first. + labels := make([]*Label, 0, len(opts.LabelIDs)) + if err = e.In("id", opts.LabelIDs).Find(&labels); err != nil { + return fmt.Errorf("find all labels [label_ids: %v]: %v", opts.LabelIDs, err) + } + + if err = opts.Issue.loadPoster(ctx); err != nil { + return err + } + + for _, label := range labels { + // Silently drop invalid labels. + if label.RepoID != opts.Repo.ID && label.OrgID != opts.Repo.OwnerID { + continue + } + + if err = newIssueLabel(ctx, opts.Issue, label, opts.Issue.Poster); err != nil { + return fmt.Errorf("addLabel [id: %d]: %v", label.ID, err) + } + } + } + + if err = NewIssueUsers(ctx, opts.Repo, opts.Issue); err != nil { + return err + } + + if len(opts.Attachments) > 0 { + attachments, err := repo_model.GetAttachmentsByUUIDs(ctx, opts.Attachments) + if err != nil { + return fmt.Errorf("getAttachmentsByUUIDs [uuids: %v]: %v", opts.Attachments, err) + } + + for i := 0; i < len(attachments); i++ { + attachments[i].IssueID = opts.Issue.ID + if _, err = e.ID(attachments[i].ID).Update(attachments[i]); err != nil { + return fmt.Errorf("update attachment [id: %d]: %v", attachments[i].ID, err) + } + } + } + if err = opts.Issue.LoadAttributes(ctx); err != nil { + return err + } + + return opts.Issue.AddCrossReferences(ctx, doer, false) +} + +// NewIssue creates new issue with labels for repository. +func NewIssue(repo *repo_model.Repository, issue *Issue, labelIDs []int64, uuids []string) (err error) { + idx, err := db.GetNextResourceIndex("issue_index", repo.ID) + if err != nil { + return fmt.Errorf("generate issue index failed: %v", err) + } + + issue.Index = idx + + ctx, committer, err := db.TxContext() + if err != nil { + return err + } + defer committer.Close() + + if err = NewIssueWithIndex(ctx, issue.Poster, NewIssueOptions{ + Repo: repo, + Issue: issue, + LabelIDs: labelIDs, + Attachments: uuids, + }); err != nil { + if repo_model.IsErrUserDoesNotHaveAccessToRepo(err) || IsErrNewIssueInsert(err) { + return err + } + return fmt.Errorf("newIssue: %v", err) + } + + if err = committer.Commit(); err != nil { + return fmt.Errorf("Commit: %v", err) + } + + return nil +} + +// GetIssueByIndex returns raw issue without loading attributes by index in a repository. +func GetIssueByIndex(repoID, index int64) (*Issue, error) { + if index < 1 { + return nil, ErrIssueNotExist{} + } + issue := &Issue{ + RepoID: repoID, + Index: index, + } + has, err := db.GetEngine(db.DefaultContext).Get(issue) + if err != nil { + return nil, err + } else if !has { + return nil, ErrIssueNotExist{0, repoID, index} + } + return issue, nil +} + +// GetIssueByForeignIndex returns raw issue by foreign ID +func GetIssueByForeignIndex(ctx context.Context, repoID, foreignIndex int64) (*Issue, error) { + reference := &foreignreference.ForeignReference{ + RepoID: repoID, + ForeignIndex: strconv.FormatInt(foreignIndex, 10), + Type: foreignreference.TypeIssue, + } + has, err := db.GetEngine(ctx).Get(reference) + if err != nil { + return nil, err + } else if !has { + return nil, foreignreference.ErrLocalIndexNotExist{ + RepoID: repoID, + ForeignIndex: foreignIndex, + Type: foreignreference.TypeIssue, + } + } + return GetIssueByIndex(repoID, reference.LocalIndex) +} + +// GetIssueWithAttrsByIndex returns issue by index in a repository. +func GetIssueWithAttrsByIndex(repoID, index int64) (*Issue, error) { + issue, err := GetIssueByIndex(repoID, index) + if err != nil { + return nil, err + } + return issue, issue.LoadAttributes(db.DefaultContext) +} + +// 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 { + return nil, err + } else if !has { + return nil, ErrIssueNotExist{id, 0, 0} + } + return issue, nil +} + +// GetIssueWithAttrsByID returns an issue with attributes by given ID. +func GetIssueWithAttrsByID(id int64) (*Issue, error) { + issue, err := GetIssueByID(db.DefaultContext, id) + if err != nil { + return nil, err + } + return issue, issue.LoadAttributes(db.DefaultContext) +} + +// GetIssuesByIDs return issues with the given IDs. +func GetIssuesByIDs(ctx context.Context, issueIDs []int64) ([]*Issue, error) { + issues := make([]*Issue, 0, 10) + return issues, db.GetEngine(ctx).In("id", issueIDs).Find(&issues) +} + +// GetIssueIDsByRepoID returns all issue ids by repo id +func GetIssueIDsByRepoID(ctx context.Context, repoID int64) ([]int64, error) { + ids := make([]int64, 0, 10) + err := db.GetEngine(ctx).Table("issue").Cols("id").Where("repo_id = ?", repoID).Find(&ids) + return ids, err +} + +// IssuesOptions represents options of an issue. +type IssuesOptions struct { //nolint + db.ListOptions + RepoID int64 // overwrites RepoCond if not 0 + RepoCond builder.Cond + AssigneeID int64 + PosterID int64 + MentionedID int64 + ReviewRequestedID int64 + MilestoneIDs []int64 + ProjectID int64 + ProjectBoardID int64 + IsClosed util.OptionalBool + IsPull util.OptionalBool + LabelIDs []int64 + IncludedLabelNames []string + ExcludedLabelNames []string + IncludeMilestones []string + SortType string + IssueIDs []int64 + UpdatedAfterUnix int64 + UpdatedBeforeUnix int64 + // prioritize issues from this repo + PriorityRepoID int64 + IsArchived util.OptionalBool + Org *organization.Organization // issues permission scope + Team *organization.Team // issues permission scope + User *user_model.User // issues permission scope +} + +// sortIssuesSession sort an issues-related session based on the provided +// sortType string +func sortIssuesSession(sess *xorm.Session, sortType string, priorityRepoID int64) { + switch sortType { + case "oldest": + sess.Asc("issue.created_unix").Asc("issue.id") + case "recentupdate": + sess.Desc("issue.updated_unix").Desc("issue.created_unix").Desc("issue.id") + case "leastupdate": + sess.Asc("issue.updated_unix").Asc("issue.created_unix").Asc("issue.id") + case "mostcomment": + sess.Desc("issue.num_comments").Desc("issue.created_unix").Desc("issue.id") + case "leastcomment": + sess.Asc("issue.num_comments").Desc("issue.created_unix").Desc("issue.id") + case "priority": + sess.Desc("issue.priority").Desc("issue.created_unix").Desc("issue.id") + case "nearduedate": + // 253370764800 is 01/01/9999 @ 12:00am (UTC) + sess.Join("LEFT", "milestone", "issue.milestone_id = milestone.id"). + OrderBy("CASE " + + "WHEN issue.deadline_unix = 0 AND (milestone.deadline_unix = 0 OR milestone.deadline_unix IS NULL) THEN 253370764800 " + + "WHEN milestone.deadline_unix = 0 OR milestone.deadline_unix IS NULL THEN issue.deadline_unix " + + "WHEN milestone.deadline_unix < issue.deadline_unix OR issue.deadline_unix = 0 THEN milestone.deadline_unix " + + "ELSE issue.deadline_unix END ASC"). + Desc("issue.created_unix"). + Desc("issue.id") + case "farduedate": + sess.Join("LEFT", "milestone", "issue.milestone_id = milestone.id"). + OrderBy("CASE " + + "WHEN milestone.deadline_unix IS NULL THEN issue.deadline_unix " + + "WHEN milestone.deadline_unix < issue.deadline_unix OR issue.deadline_unix = 0 THEN milestone.deadline_unix " + + "ELSE issue.deadline_unix END DESC"). + Desc("issue.created_unix"). + Desc("issue.id") + case "priorityrepo": + sess.OrderBy("CASE "+ + "WHEN issue.repo_id = ? THEN 1 "+ + "ELSE 2 END ASC", priorityRepoID). + Desc("issue.created_unix"). + Desc("issue.id") + case "project-column-sorting": + sess.Asc("project_issue.sorting").Desc("issue.created_unix").Desc("issue.id") + default: + sess.Desc("issue.created_unix").Desc("issue.id") + } +} + +func (opts *IssuesOptions) setupSessionWithLimit(sess *xorm.Session) { + if opts.Page >= 0 && opts.PageSize > 0 { + var start int + if opts.Page == 0 { + start = 0 + } else { + start = (opts.Page - 1) * opts.PageSize + } + sess.Limit(opts.PageSize, start) + } + opts.setupSessionNoLimit(sess) +} + +func (opts *IssuesOptions) setupSessionNoLimit(sess *xorm.Session) { + if len(opts.IssueIDs) > 0 { + sess.In("issue.id", opts.IssueIDs) + } + + if opts.RepoID != 0 { + opts.RepoCond = builder.Eq{"issue.repo_id": opts.RepoID} + } + if opts.RepoCond != nil { + sess.And(opts.RepoCond) + } + + if !opts.IsClosed.IsNone() { + sess.And("issue.is_closed=?", opts.IsClosed.IsTrue()) + } + + if opts.AssigneeID > 0 { + applyAssigneeCondition(sess, opts.AssigneeID) + } + + if opts.PosterID > 0 { + applyPosterCondition(sess, opts.PosterID) + } + + if opts.MentionedID > 0 { + applyMentionedCondition(sess, opts.MentionedID) + } + + if opts.ReviewRequestedID > 0 { + applyReviewRequestedCondition(sess, opts.ReviewRequestedID) + } + + if len(opts.MilestoneIDs) > 0 { + sess.In("issue.milestone_id", opts.MilestoneIDs) + } + + if opts.UpdatedAfterUnix != 0 { + sess.And(builder.Gte{"issue.updated_unix": opts.UpdatedAfterUnix}) + } + if opts.UpdatedBeforeUnix != 0 { + sess.And(builder.Lte{"issue.updated_unix": opts.UpdatedBeforeUnix}) + } + + if opts.ProjectID > 0 { + sess.Join("INNER", "project_issue", "issue.id = project_issue.issue_id"). + And("project_issue.project_id=?", opts.ProjectID) + } + + if opts.ProjectBoardID != 0 { + if opts.ProjectBoardID > 0 { + sess.In("issue.id", builder.Select("issue_id").From("project_issue").Where(builder.Eq{"project_board_id": opts.ProjectBoardID})) + } else { + sess.In("issue.id", builder.Select("issue_id").From("project_issue").Where(builder.Eq{"project_board_id": 0})) + } + } + + switch opts.IsPull { + case util.OptionalBoolTrue: + sess.And("issue.is_pull=?", true) + case util.OptionalBoolFalse: + sess.And("issue.is_pull=?", false) + } + + if opts.IsArchived != util.OptionalBoolNone { + sess.And(builder.Eq{"repository.is_archived": opts.IsArchived.IsTrue()}) + } + + if opts.LabelIDs != nil { + for i, labelID := range opts.LabelIDs { + if labelID > 0 { + sess.Join("INNER", fmt.Sprintf("issue_label il%d", i), + fmt.Sprintf("issue.id = il%[1]d.issue_id AND il%[1]d.label_id = %[2]d", i, labelID)) + } else { + sess.Where("issue.id not in (select issue_id from issue_label where label_id = ?)", -labelID) + } + } + } + + if len(opts.IncludedLabelNames) > 0 { + sess.In("issue.id", BuildLabelNamesIssueIDsCondition(opts.IncludedLabelNames)) + } + + if len(opts.ExcludedLabelNames) > 0 { + sess.And(builder.NotIn("issue.id", BuildLabelNamesIssueIDsCondition(opts.ExcludedLabelNames))) + } + + if len(opts.IncludeMilestones) > 0 { + sess.In("issue.milestone_id", + builder.Select("id"). + From("milestone"). + Where(builder.In("name", opts.IncludeMilestones))) + } + + if opts.User != nil { + sess.And(issuePullAccessibleRepoCond("issue.repo_id", opts.User.ID, opts.Org, opts.Team, opts.IsPull.IsTrue())) + } +} + +// teamUnitsRepoCond returns query condition for those repo id in the special org team with special units access +func teamUnitsRepoCond(id string, userID, orgID, teamID int64, units ...unit.Type) builder.Cond { + return builder.In(id, + builder.Select("repo_id").From("team_repo").Where( + builder.Eq{ + "team_id": teamID, + }.And( + builder.Or( + // Check if the user is member of the team. + builder.In( + "team_id", builder.Select("team_id").From("team_user").Where( + builder.Eq{ + "uid": userID, + }, + ), + ), + // Check if the user is in the owner team of the organisation. + builder.Exists(builder.Select("team_id").From("team_user"). + Where(builder.Eq{ + "org_id": orgID, + "team_id": builder.Select("id").From("team").Where( + builder.Eq{ + "org_id": orgID, + "lower_name": strings.ToLower(organization.OwnerTeamName), + }), + "uid": userID, + }), + ), + )).And( + builder.In( + "team_id", builder.Select("team_id").From("team_unit").Where( + builder.Eq{ + "`team_unit`.org_id": orgID, + }.And( + builder.In("`team_unit`.type", units), + ), + ), + ), + ), + )) +} + +// issuePullAccessibleRepoCond userID must not be zero, this condition require join repository table +func issuePullAccessibleRepoCond(repoIDstr string, userID int64, org *organization.Organization, team *organization.Team, isPull bool) builder.Cond { + cond := builder.NewCond() + unitType := unit.TypeIssues + if isPull { + unitType = unit.TypePullRequests + } + if org != nil { + if team != nil { + cond = cond.And(teamUnitsRepoCond(repoIDstr, userID, org.ID, team.ID, unitType)) // special team member repos + } else { + cond = cond.And( + builder.Or( + repo_model.UserOrgUnitRepoCond(repoIDstr, userID, org.ID, unitType), // team member repos + repo_model.UserOrgPublicUnitRepoCond(userID, org.ID), // user org public non-member repos, TODO: check repo has issues + ), + ) + } + } else { + cond = cond.And( + builder.Or( + repo_model.UserOwnedRepoCond(userID), // owned repos + repo_model.UserCollaborationRepoCond(repoIDstr, userID), // collaboration repos + repo_model.UserAssignedRepoCond(repoIDstr, userID), // user has been assigned accessible public repos + repo_model.UserMentionedRepoCond(repoIDstr, userID), // user has been mentioned accessible public repos + repo_model.UserCreateIssueRepoCond(repoIDstr, userID, isPull), // user has created issue/pr accessible public repos + ), + ) + } + return cond +} + +func applyAssigneeCondition(sess *xorm.Session, assigneeID int64) *xorm.Session { + return sess.Join("INNER", "issue_assignees", "issue.id = issue_assignees.issue_id"). + And("issue_assignees.assignee_id = ?", assigneeID) +} + +func applyPosterCondition(sess *xorm.Session, posterID int64) *xorm.Session { + return sess.And("issue.poster_id=?", posterID) +} + +func applyMentionedCondition(sess *xorm.Session, mentionedID int64) *xorm.Session { + return sess.Join("INNER", "issue_user", "issue.id = issue_user.issue_id"). + And("issue_user.is_mentioned = ?", true). + And("issue_user.uid = ?", mentionedID) +} + +func applyReviewRequestedCondition(sess *xorm.Session, reviewRequestedID int64) *xorm.Session { + return sess.Join("INNER", []string{"review", "r"}, "issue.id = r.issue_id"). + And("issue.poster_id <> ?", reviewRequestedID). + And("r.type = ?", ReviewTypeRequest). + And("r.reviewer_id = ? and r.id in (select max(id) from review where issue_id = r.issue_id and reviewer_id = r.reviewer_id and type in (?, ?, ?))"+ + " or r.reviewer_team_id in (select team_id from team_user where uid = ?)", + reviewRequestedID, ReviewTypeApprove, ReviewTypeReject, ReviewTypeRequest, reviewRequestedID) +} + +// CountIssuesByRepo map from repoID to number of issues matching the options +func CountIssuesByRepo(opts *IssuesOptions) (map[int64]int64, error) { + e := db.GetEngine(db.DefaultContext) + + sess := e.Join("INNER", "repository", "`issue`.repo_id = `repository`.id") + + opts.setupSessionNoLimit(sess) + + countsSlice := make([]*struct { + RepoID int64 + Count int64 + }, 0, 10) + if err := sess.GroupBy("issue.repo_id"). + Select("issue.repo_id AS repo_id, COUNT(*) AS count"). + Table("issue"). + Find(&countsSlice); err != nil { + return nil, fmt.Errorf("unable to CountIssuesByRepo: %w", err) + } + + countMap := make(map[int64]int64, len(countsSlice)) + for _, c := range countsSlice { + countMap[c.RepoID] = c.Count + } + return countMap, nil +} + +// GetRepoIDsForIssuesOptions find all repo ids for the given options +func GetRepoIDsForIssuesOptions(opts *IssuesOptions, user *user_model.User) ([]int64, error) { + repoIDs := make([]int64, 0, 5) + e := db.GetEngine(db.DefaultContext) + + sess := e.Join("INNER", "repository", "`issue`.repo_id = `repository`.id") + + opts.setupSessionNoLimit(sess) + + accessCond := repo_model.AccessibleRepositoryCondition(user) + if err := sess.Where(accessCond). + Distinct("issue.repo_id"). + Table("issue"). + Find(&repoIDs); err != nil { + return nil, fmt.Errorf("unable to GetRepoIDsForIssuesOptions: %w", err) + } + + return repoIDs, nil +} + +// Issues returns a list of issues by given conditions. +func Issues(opts *IssuesOptions) ([]*Issue, error) { + e := db.GetEngine(db.DefaultContext) + + sess := e.Join("INNER", "repository", "`issue`.repo_id = `repository`.id") + opts.setupSessionWithLimit(sess) + + sortIssuesSession(sess, opts.SortType, opts.PriorityRepoID) + + issues := make([]*Issue, 0, opts.ListOptions.PageSize) + if err := sess.Find(&issues); err != nil { + return nil, fmt.Errorf("unable to query Issues: %w", err) + } + + if err := IssueList(issues).LoadAttributes(); err != nil { + return nil, fmt.Errorf("unable to LoadAttributes for Issues: %w", err) + } + + return issues, nil +} + +// CountIssues number return of issues by given conditions. +func CountIssues(opts *IssuesOptions) (int64, error) { + e := db.GetEngine(db.DefaultContext) + + sess := e.Select("COUNT(issue.id) AS count").Table("issue") + sess.Join("INNER", "repository", "`issue`.repo_id = `repository`.id") + opts.setupSessionNoLimit(sess) + + return sess.Count() +} + +// GetParticipantsIDsByIssueID returns the IDs of all users who participated in comments of an issue, +// but skips joining with `user` for performance reasons. +// User permissions must be verified elsewhere if required. +func GetParticipantsIDsByIssueID(issueID int64) ([]int64, error) { + userIDs := make([]int64, 0, 5) + return userIDs, db.GetEngine(db.DefaultContext).Table("comment"). + Cols("poster_id"). + Where("issue_id = ?", issueID). + And("type in (?,?,?)", CommentTypeComment, CommentTypeCode, CommentTypeReview). + Distinct("poster_id"). + Find(&userIDs) +} + +// 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) + if err != nil { + log.Error(err.Error()) + return false + } + return util.IsInt64InSlice(user.ID, userIDs) +} + +// UpdateIssueMentions updates issue-user relations for mentioned users. +func UpdateIssueMentions(ctx context.Context, issueID int64, mentions []*user_model.User) error { + if len(mentions) == 0 { + return nil + } + ids := make([]int64, len(mentions)) + for i, u := range mentions { + ids[i] = u.ID + } + if err := UpdateIssueUsersByMentions(ctx, issueID, ids); err != nil { + return fmt.Errorf("UpdateIssueUsersByMentions: %v", err) + } + return nil +} + +// IssueStats represents issue statistic information. +type IssueStats struct { + OpenCount, ClosedCount int64 + YourRepositoriesCount int64 + AssignCount int64 + CreateCount int64 + MentionCount int64 + ReviewRequestedCount int64 +} + +// Filter modes. +const ( + FilterModeAll = iota + FilterModeAssign + FilterModeCreate + FilterModeMention + FilterModeReviewRequested + FilterModeYourRepositories +) + +// IssueStatsOptions contains parameters accepted by GetIssueStats. +type IssueStatsOptions struct { + RepoID int64 + Labels string + MilestoneID int64 + AssigneeID int64 + MentionedID int64 + PosterID int64 + ReviewRequestedID int64 + IsPull util.OptionalBool + IssueIDs []int64 +} + +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 +) + +// GetIssueStats returns issue statistic information by given conditions. +func GetIssueStats(opts *IssueStatsOptions) (*IssueStats, error) { + if len(opts.IssueIDs) <= MaxQueryParameters { + return getIssueStatsChunk(opts, opts.IssueIDs) + } + + // If too long a list of IDs is provided, we get the statistics in + // smaller chunks and get accumulates. Note: this could potentially + // get us invalid results. The alternative is to insert the list of + // ids in a temporary table and join from them. + accum := &IssueStats{} + for i := 0; i < len(opts.IssueIDs); { + chunk := i + MaxQueryParameters + if chunk > len(opts.IssueIDs) { + chunk = len(opts.IssueIDs) + } + stats, err := getIssueStatsChunk(opts, opts.IssueIDs[i:chunk]) + if err != nil { + return nil, err + } + accum.OpenCount += stats.OpenCount + accum.ClosedCount += stats.ClosedCount + accum.YourRepositoriesCount += stats.YourRepositoriesCount + accum.AssignCount += stats.AssignCount + accum.CreateCount += stats.CreateCount + accum.OpenCount += stats.MentionCount + accum.ReviewRequestedCount += stats.ReviewRequestedCount + i = chunk + } + return accum, nil +} + +func getIssueStatsChunk(opts *IssueStatsOptions, issueIDs []int64) (*IssueStats, error) { + stats := &IssueStats{} + + countSession := func(opts *IssueStatsOptions, issueIDs []int64) *xorm.Session { + sess := db.GetEngine(db.DefaultContext). + Where("issue.repo_id = ?", opts.RepoID) + + if len(issueIDs) > 0 { + sess.In("issue.id", issueIDs) + } + + if len(opts.Labels) > 0 && opts.Labels != "0" { + labelIDs, err := base.StringsToInt64s(strings.Split(opts.Labels, ",")) + if err != nil { + log.Warn("Malformed Labels argument: %s", opts.Labels) + } else { + for i, labelID := range labelIDs { + if labelID > 0 { + sess.Join("INNER", fmt.Sprintf("issue_label il%d", i), + fmt.Sprintf("issue.id = il%[1]d.issue_id AND il%[1]d.label_id = %[2]d", i, labelID)) + } else { + sess.Where("issue.id NOT IN (SELECT issue_id FROM issue_label WHERE label_id = ?)", -labelID) + } + } + } + } + + if opts.MilestoneID > 0 { + sess.And("issue.milestone_id = ?", opts.MilestoneID) + } + + if opts.AssigneeID > 0 { + applyAssigneeCondition(sess, opts.AssigneeID) + } + + if opts.PosterID > 0 { + applyPosterCondition(sess, opts.PosterID) + } + + if opts.MentionedID > 0 { + applyMentionedCondition(sess, opts.MentionedID) + } + + if opts.ReviewRequestedID > 0 { + applyReviewRequestedCondition(sess, opts.ReviewRequestedID) + } + + switch opts.IsPull { + case util.OptionalBoolTrue: + sess.And("issue.is_pull=?", true) + case util.OptionalBoolFalse: + sess.And("issue.is_pull=?", false) + } + + return sess + } + + var err error + stats.OpenCount, err = countSession(opts, issueIDs). + And("issue.is_closed = ?", false). + Count(new(Issue)) + if err != nil { + return stats, err + } + stats.ClosedCount, err = countSession(opts, issueIDs). + And("issue.is_closed = ?", true). + Count(new(Issue)) + return stats, err +} + +// UserIssueStatsOptions contains parameters accepted by GetUserIssueStats. +type UserIssueStatsOptions struct { + UserID int64 + RepoIDs []int64 + FilterMode int + IsPull bool + IsClosed bool + IssueIDs []int64 + IsArchived util.OptionalBool + LabelIDs []int64 + RepoCond builder.Cond + Org *organization.Organization + Team *organization.Team +} + +// GetUserIssueStats returns issue statistic information for dashboard by given conditions. +func GetUserIssueStats(opts UserIssueStatsOptions) (*IssueStats, error) { + var err error + stats := &IssueStats{} + + cond := builder.NewCond() + cond = cond.And(builder.Eq{"issue.is_pull": opts.IsPull}) + if len(opts.RepoIDs) > 0 { + cond = cond.And(builder.In("issue.repo_id", opts.RepoIDs)) + } + if len(opts.IssueIDs) > 0 { + cond = cond.And(builder.In("issue.id", opts.IssueIDs)) + } + if opts.RepoCond != nil { + cond = cond.And(opts.RepoCond) + } + + if opts.UserID > 0 { + cond = cond.And(issuePullAccessibleRepoCond("issue.repo_id", opts.UserID, opts.Org, opts.Team, opts.IsPull)) + } + + sess := func(cond builder.Cond) *xorm.Session { + s := db.GetEngine(db.DefaultContext).Where(cond) + if len(opts.LabelIDs) > 0 { + s.Join("INNER", "issue_label", "issue_label.issue_id = issue.id"). + In("issue_label.label_id", opts.LabelIDs) + } + if opts.UserID > 0 || opts.IsArchived != util.OptionalBoolNone { + s.Join("INNER", "repository", "issue.repo_id = repository.id") + if opts.IsArchived != util.OptionalBoolNone { + s.And(builder.Eq{"repository.is_archived": opts.IsArchived.IsTrue()}) + } + } + return s + } + + switch opts.FilterMode { + case FilterModeAll, FilterModeYourRepositories: + stats.OpenCount, err = sess(cond). + And("issue.is_closed = ?", false). + Count(new(Issue)) + if err != nil { + return nil, err + } + stats.ClosedCount, err = sess(cond). + And("issue.is_closed = ?", true). + Count(new(Issue)) + if err != nil { + return nil, err + } + case FilterModeAssign: + stats.OpenCount, err = applyAssigneeCondition(sess(cond), opts.UserID). + And("issue.is_closed = ?", false). + Count(new(Issue)) + if err != nil { + return nil, err + } + stats.ClosedCount, err = applyAssigneeCondition(sess(cond), opts.UserID). + And("issue.is_closed = ?", true). + Count(new(Issue)) + if err != nil { + return nil, err + } + case FilterModeCreate: + stats.OpenCount, err = applyPosterCondition(sess(cond), opts.UserID). + And("issue.is_closed = ?", false). + Count(new(Issue)) + if err != nil { + return nil, err + } + stats.ClosedCount, err = applyPosterCondition(sess(cond), opts.UserID). + And("issue.is_closed = ?", true). + Count(new(Issue)) + if err != nil { + return nil, err + } + case FilterModeMention: + stats.OpenCount, err = applyMentionedCondition(sess(cond), opts.UserID). + And("issue.is_closed = ?", false). + Count(new(Issue)) + if err != nil { + return nil, err + } + stats.ClosedCount, err = applyMentionedCondition(sess(cond), opts.UserID). + And("issue.is_closed = ?", true). + Count(new(Issue)) + if err != nil { + return nil, err + } + case FilterModeReviewRequested: + stats.OpenCount, err = applyReviewRequestedCondition(sess(cond), opts.UserID). + And("issue.is_closed = ?", false). + Count(new(Issue)) + if err != nil { + return nil, err + } + stats.ClosedCount, err = applyReviewRequestedCondition(sess(cond), opts.UserID). + And("issue.is_closed = ?", true). + Count(new(Issue)) + if err != nil { + return nil, err + } + } + + cond = cond.And(builder.Eq{"issue.is_closed": opts.IsClosed}) + stats.AssignCount, err = applyAssigneeCondition(sess(cond), opts.UserID).Count(new(Issue)) + if err != nil { + return nil, err + } + + stats.CreateCount, err = applyPosterCondition(sess(cond), opts.UserID).Count(new(Issue)) + if err != nil { + return nil, err + } + + stats.MentionCount, err = applyMentionedCondition(sess(cond), opts.UserID).Count(new(Issue)) + if err != nil { + return nil, err + } + + stats.YourRepositoriesCount, err = sess(cond).Count(new(Issue)) + if err != nil { + return nil, err + } + + stats.ReviewRequestedCount, err = applyReviewRequestedCondition(sess(cond), opts.UserID).Count(new(Issue)) + if err != nil { + return nil, err + } + + return stats, nil +} + +// GetRepoIssueStats returns number of open and closed repository issues by given filter mode. +func GetRepoIssueStats(repoID, uid int64, filterMode int, isPull bool) (numOpen, numClosed int64) { + countSession := func(isClosed, isPull bool, repoID int64) *xorm.Session { + sess := db.GetEngine(db.DefaultContext). + Where("is_closed = ?", isClosed). + And("is_pull = ?", isPull). + And("repo_id = ?", repoID) + + return sess + } + + openCountSession := countSession(false, isPull, repoID) + closedCountSession := countSession(true, isPull, repoID) + + switch filterMode { + case FilterModeAssign: + applyAssigneeCondition(openCountSession, uid) + applyAssigneeCondition(closedCountSession, uid) + case FilterModeCreate: + applyPosterCondition(openCountSession, uid) + applyPosterCondition(closedCountSession, uid) + } + + openResult, _ := openCountSession.Count(new(Issue)) + closedResult, _ := closedCountSession.Count(new(Issue)) + + return openResult, closedResult +} + +// SearchIssueIDsByKeyword search issues on database +func SearchIssueIDsByKeyword(ctx context.Context, kw string, repoIDs []int64, limit, start int) (int64, []int64, error) { + repoCond := builder.In("repo_id", repoIDs) + subQuery := builder.Select("id").From("issue").Where(repoCond) + // SQLite's UPPER function only transforms ASCII letters. + if setting.Database.UseSQLite3 { + kw = util.ToUpperASCII(kw) + } else { + kw = strings.ToUpper(kw) + } + cond := builder.And( + repoCond, + builder.Or( + builder.Like{"UPPER(name)", kw}, + builder.Like{"UPPER(content)", kw}, + builder.In("id", builder.Select("issue_id"). + From("comment"). + Where(builder.And( + builder.Eq{"type": CommentTypeComment}, + builder.In("issue_id", subQuery), + builder.Like{"UPPER(content)", kw}, + )), + ), + ), + ) + + ids := make([]int64, 0, limit) + res := make([]struct { + ID int64 + UpdatedUnix int64 + }, 0, limit) + err := db.GetEngine(ctx).Distinct("id", "updated_unix").Table("issue").Where(cond). + OrderBy("`updated_unix` DESC").Limit(limit, start). + Find(&res) + if err != nil { + return 0, nil, err + } + for _, r := range res { + ids = append(ids, r.ID) + } + + total, err := db.GetEngine(ctx).Distinct("id").Table("issue").Where(cond).Count() + if err != nil { + return 0, nil, err + } + + return total, ids, nil +} + +// UpdateIssueByAPI updates all allowed fields of given issue. +// If the issue status is changed a statusChangeComment is returned +// similarly if the title is changed the titleChanged bool is set to true +func UpdateIssueByAPI(issue *Issue, doer *user_model.User) (statusChangeComment *Comment, titleChanged bool, err error) { + ctx, committer, err := db.TxContext() + if err != nil { + return nil, false, err + } + defer committer.Close() + + if err := issue.LoadRepo(ctx); err != nil { + return nil, false, fmt.Errorf("loadRepo: %v", err) + } + + // Reload the issue + currentIssue, err := GetIssueByID(ctx, issue.ID) + if err != nil { + return nil, false, err + } + + if _, err := db.GetEngine(ctx).ID(issue.ID).Cols( + "name", "content", "milestone_id", "priority", + "deadline_unix", "updated_unix", "is_locked"). + Update(issue); err != nil { + return nil, false, err + } + + titleChanged = currentIssue.Title != issue.Title + if titleChanged { + opts := &CreateCommentOptions{ + Type: CommentTypeChangeTitle, + Doer: doer, + Repo: issue.Repo, + Issue: issue, + OldTitle: currentIssue.Title, + NewTitle: issue.Title, + } + _, err := CreateCommentCtx(ctx, opts) + if err != nil { + return nil, false, fmt.Errorf("createComment: %v", err) + } + } + + if currentIssue.IsClosed != issue.IsClosed { + statusChangeComment, err = doChangeIssueStatus(ctx, issue, doer, false) + if err != nil { + return nil, false, err + } + } + + if err := issue.AddCrossReferences(ctx, doer, true); err != nil { + return nil, false, err + } + return statusChangeComment, titleChanged, committer.Commit() +} + +// UpdateIssueDeadline updates an issue deadline and adds comments. Setting a deadline to 0 means deleting it. +func UpdateIssueDeadline(issue *Issue, deadlineUnix timeutil.TimeStamp, doer *user_model.User) (err error) { + // if the deadline hasn't changed do nothing + if issue.DeadlineUnix == deadlineUnix { + return nil + } + ctx, committer, err := db.TxContext() + if err != nil { + return err + } + defer committer.Close() + + // Update the deadline + if err = UpdateIssueCols(ctx, &Issue{ID: issue.ID, DeadlineUnix: deadlineUnix}, "deadline_unix"); err != nil { + return err + } + + // Make the comment + if _, err = createDeadlineComment(ctx, doer, issue, deadlineUnix); err != nil { + return fmt.Errorf("createRemovedDueDateComment: %v", err) + } + + return committer.Commit() +} + +// 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 { + 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) { + if issue == nil { + return nil, nil + } + userIDs := make([]int64, 0, 5) + if err := db.GetEngine(ctx).Table("comment").Cols("poster_id"). + Where("`comment`.issue_id = ?", issue.ID). + And("`comment`.type in (?,?,?)", CommentTypeComment, CommentTypeCode, CommentTypeReview). + And("`user`.is_active = ?", true). + And("`user`.prohibit_login = ?", false). + Join("INNER", "`user`", "`user`.id = `comment`.poster_id"). + Distinct("poster_id"). + Find(&userIDs); err != nil { + return nil, fmt.Errorf("get poster IDs: %v", err) + } + if !util.IsInt64InSlice(issue.PosterID, userIDs) { + return append(userIDs, issue.PosterID), nil + } + return userIDs, nil +} + +// BlockedByDependencies finds all Dependencies an issue is blocked by +func (issue *Issue) BlockedByDependencies(ctx context.Context) (issueDeps []*DependencyInfo, err error) { + err = db.GetEngine(ctx). + Table("issue"). + Join("INNER", "repository", "repository.id = issue.repo_id"). + Join("INNER", "issue_dependency", "issue_dependency.dependency_id = issue.id"). + Where("issue_id = ?", issue.ID). + // sort by repo id then created date, with the issues of the same repo at the beginning of the list + OrderBy("CASE WHEN issue.repo_id = ? THEN 0 ELSE issue.repo_id END, issue.created_unix DESC", issue.RepoID). + Find(&issueDeps) + + for _, depInfo := range issueDeps { + depInfo.Issue.Repo = &depInfo.Repository + } + + return issueDeps, err +} + +// BlockingDependencies returns all blocking dependencies, aka all other issues a given issue blocks +func (issue *Issue) BlockingDependencies(ctx context.Context) (issueDeps []*DependencyInfo, err error) { + err = db.GetEngine(ctx). + Table("issue"). + Join("INNER", "repository", "repository.id = issue.repo_id"). + Join("INNER", "issue_dependency", "issue_dependency.issue_id = issue.id"). + Where("dependency_id = ?", issue.ID). + // sort by repo id then created date, with the issues of the same repo at the beginning of the list + OrderBy("CASE WHEN issue.repo_id = ? THEN 0 ELSE issue.repo_id END, issue.created_unix DESC", issue.RepoID). + Find(&issueDeps) + + for _, depInfo := range issueDeps { + depInfo.Issue.Repo = &depInfo.Repository + } + + return issueDeps, err +} + +func updateIssueClosedNum(ctx context.Context, issue *Issue) (err error) { + if issue.IsPull { + err = repo_model.StatsCorrectNumClosed(ctx, issue.RepoID, true, "num_closed_pulls") + } else { + err = repo_model.StatsCorrectNumClosed(ctx, issue.RepoID, false, "num_closed_issues") + } + return +} + +// FindAndUpdateIssueMentions finds users mentioned in the given content string, and saves them in the database. +func FindAndUpdateIssueMentions(ctx context.Context, issue *Issue, doer *user_model.User, content string) (mentions []*user_model.User, err error) { + rawMentions := references.FindAllMentionsMarkdown(content) + mentions, err = ResolveIssueMentionsByVisibility(ctx, issue, doer, rawMentions) + if err != nil { + return nil, fmt.Errorf("UpdateIssueMentions [%d]: %v", issue.ID, err) + } + if err = UpdateIssueMentions(ctx, issue.ID, mentions); err != nil { + return nil, fmt.Errorf("UpdateIssueMentions [%d]: %v", issue.ID, err) + } + return +} + +// ResolveIssueMentionsByVisibility returns the users mentioned in an issue, removing those that +// don't have access to reading it. Teams are expanded into their users, but organizations are ignored. +func ResolveIssueMentionsByVisibility(ctx context.Context, issue *Issue, doer *user_model.User, mentions []string) (users []*user_model.User, err error) { + if len(mentions) == 0 { + return + } + if err = issue.LoadRepo(ctx); err != nil { + return + } + + resolved := make(map[string]bool, 10) + var mentionTeams []string + + if err := issue.Repo.GetOwner(ctx); err != nil { + return nil, err + } + + repoOwnerIsOrg := issue.Repo.Owner.IsOrganization() + if repoOwnerIsOrg { + mentionTeams = make([]string, 0, 5) + } + + resolved[doer.LowerName] = true + for _, name := range mentions { + name := strings.ToLower(name) + if _, ok := resolved[name]; ok { + continue + } + if repoOwnerIsOrg && strings.Contains(name, "/") { + names := strings.Split(name, "/") + if len(names) < 2 || names[0] != issue.Repo.Owner.LowerName { + continue + } + mentionTeams = append(mentionTeams, names[1]) + resolved[name] = true + } else { + resolved[name] = false + } + } + + if issue.Repo.Owner.IsOrganization() && len(mentionTeams) > 0 { + teams := make([]*organization.Team, 0, len(mentionTeams)) + if err := db.GetEngine(ctx). + Join("INNER", "team_repo", "team_repo.team_id = team.id"). + Where("team_repo.repo_id=?", issue.Repo.ID). + In("team.lower_name", mentionTeams). + Find(&teams); err != nil { + return nil, fmt.Errorf("find mentioned teams: %v", err) + } + if len(teams) != 0 { + checked := make([]int64, 0, len(teams)) + unittype := unit.TypeIssues + if issue.IsPull { + unittype = unit.TypePullRequests + } + for _, team := range teams { + if team.AccessMode >= perm.AccessModeAdmin { + checked = append(checked, team.ID) + resolved[issue.Repo.Owner.LowerName+"/"+team.LowerName] = true + continue + } + has, err := db.GetEngine(ctx).Get(&organization.TeamUnit{OrgID: issue.Repo.Owner.ID, TeamID: team.ID, Type: unittype}) + if err != nil { + return nil, fmt.Errorf("get team units (%d): %v", team.ID, err) + } + if has { + checked = append(checked, team.ID) + resolved[issue.Repo.Owner.LowerName+"/"+team.LowerName] = true + } + } + if len(checked) != 0 { + teamusers := make([]*user_model.User, 0, 20) + if err := db.GetEngine(ctx). + Join("INNER", "team_user", "team_user.uid = `user`.id"). + In("`team_user`.team_id", checked). + And("`user`.is_active = ?", true). + And("`user`.prohibit_login = ?", false). + Find(&teamusers); err != nil { + return nil, fmt.Errorf("get teams users: %v", err) + } + if len(teamusers) > 0 { + users = make([]*user_model.User, 0, len(teamusers)) + for _, user := range teamusers { + if already, ok := resolved[user.LowerName]; !ok || !already { + users = append(users, user) + resolved[user.LowerName] = true + } + } + } + } + } + } + + // Remove names already in the list to avoid querying the database if pending names remain + mentionUsers := make([]string, 0, len(resolved)) + for name, already := range resolved { + if !already { + mentionUsers = append(mentionUsers, name) + } + } + if len(mentionUsers) == 0 { + return + } + + if users == nil { + users = make([]*user_model.User, 0, len(mentionUsers)) + } + + unchecked := make([]*user_model.User, 0, len(mentionUsers)) + if err := db.GetEngine(ctx). + Where("`user`.is_active = ?", true). + And("`user`.prohibit_login = ?", false). + In("`user`.lower_name", mentionUsers). + Find(&unchecked); err != nil { + return nil, fmt.Errorf("find mentioned users: %v", err) + } + for _, user := range unchecked { + if already := resolved[user.LowerName]; already || user.IsOrganization() { + continue + } + // Normal users must have read access to the referencing issue + perm, err := access_model.GetUserRepoPermission(ctx, issue.Repo, user) + if err != nil { + return nil, fmt.Errorf("GetUserRepoPermission [%d]: %v", user.ID, err) + } + if !perm.CanReadIssuesOrPulls(issue.IsPull) { + continue + } + users = append(users, user) + } + + return +} + +// UpdateIssuesMigrationsByType updates all migrated repositories' issues from gitServiceType to replace originalAuthorID to posterID +func UpdateIssuesMigrationsByType(gitServiceType api.GitServiceType, originalAuthorID string, posterID int64) error { + _, err := db.GetEngine(db.DefaultContext).Table("issue"). + Where("repo_id IN (SELECT id FROM repository WHERE original_service_type = ?)", gitServiceType). + And("original_author_id = ?", originalAuthorID). + Update(map[string]interface{}{ + "poster_id": posterID, + "original_author": "", + "original_author_id": 0, + }) + 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"). + Where("original_author_id = ?", originalAuthorID). + And(migratedIssueCond(gitServiceType)). + Update(map[string]interface{}{ + "user_id": userID, + "original_author": "", + "original_author_id": 0, + }) + return err +} + +// 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(&ContentHistory{}); err != nil { + return + } + + // Delete comments and attachments + if _, err = sess.In("issue_id", deleteCond). + Delete(&Comment{}); err != nil { + return + } + + // Dependencies for issues in this repository + if _, err = sess.In("issue_id", deleteCond). + Delete(&IssueDependency{}); err != nil { + return + } + + // Delete dependencies for issues in other repositories + if _, err = sess.In("dependency_id", deleteCond). + Delete(&IssueDependency{}); err != nil { + return + } + + if _, err = sess.In("issue_id", deleteCond). + Delete(&IssueUser{}); err != nil { + return + } + + if _, err = sess.In("issue_id", deleteCond). + Delete(&Reaction{}); err != nil { + return + } + + if _, err = sess.In("issue_id", deleteCond). + Delete(&IssueWatch{}); err != nil { + return + } + + if _, err = sess.In("issue_id", deleteCond). + Delete(&Stopwatch{}); err != nil { + return + } + + if _, err = sess.In("issue_id", deleteCond). + Delete(&TrackedTime{}); err != nil { + return + } + + if _, err = sess.In("issue_id", deleteCond). + Delete(&project_model.ProjectIssue{}); err != nil { + return + } + + if _, err = sess.In("dependent_issue_id", deleteCond). + Delete(&Comment{}); err != nil { + return + } + + var attachments []*repo_model.Attachment + if err = sess.In("issue_id", deleteCond). + Find(&attachments); err != nil { + return + } + + for j := range attachments { + attachmentPaths = append(attachmentPaths, attachments[j].RelativePath()) + } + + if _, err = sess.In("issue_id", deleteCond). + Delete(&repo_model.Attachment{}); err != nil { + return + } + + if _, err = db.DeleteByBean(ctx, &Issue{RepoID: repoID}); err != nil { + return + } + + return +} + +// RemapExternalUser ExternalUserRemappable interface +func (issue *Issue) RemapExternalUser(externalName string, externalID, userID int64) error { + issue.OriginalAuthor = externalName + issue.OriginalAuthorID = externalID + issue.PosterID = userID + return nil +} + +// GetUserID ExternalUserRemappable interface +func (issue *Issue) GetUserID() int64 { return issue.PosterID } + +// GetExternalName ExternalUserRemappable interface +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/issues/issue_list.go b/models/issues/issue_list.go new file mode 100644 index 0000000000..20e9949b66 --- /dev/null +++ b/models/issues/issue_list.go @@ -0,0 +1,565 @@ +// 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 ( + "context" + "fmt" + + "code.gitea.io/gitea/models/db" + repo_model "code.gitea.io/gitea/models/repo" + user_model "code.gitea.io/gitea/models/user" + "code.gitea.io/gitea/modules/container" + + "xorm.io/builder" +) + +// IssueList defines a list of issues +type IssueList []*Issue + +// 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)) + for _, issue := range issues { + if issue.Repo == nil { + repoIDs[issue.RepoID] = struct{}{} + } + if issue.PullRequest != nil && issue.PullRequest.HeadRepo == nil { + repoIDs[issue.PullRequest.HeadRepoID] = struct{}{} + } + } + return container.KeysInt64(repoIDs) +} + +func (issues IssueList) loadRepositories(ctx context.Context) ([]*repo_model.Repository, error) { + if len(issues) == 0 { + return nil, nil + } + + repoIDs := issues.getRepoIDs() + repoMaps := make(map[int64]*repo_model.Repository, len(repoIDs)) + left := len(repoIDs) + for left > 0 { + limit := db.DefaultMaxInSize + if left < limit { + limit = left + } + err := db.GetEngine(ctx). + In("id", repoIDs[:limit]). + Find(&repoMaps) + if err != nil { + return nil, fmt.Errorf("find repository: %v", err) + } + left -= limit + repoIDs = repoIDs[limit:] + } + + for _, issue := range issues { + if issue.Repo == nil { + issue.Repo = repoMaps[issue.RepoID] + } else { + repoMaps[issue.RepoID] = issue.Repo + } + if issue.PullRequest != nil { + issue.PullRequest.BaseRepo = issue.Repo + if issue.PullRequest.HeadRepo == nil { + issue.PullRequest.HeadRepo = repoMaps[issue.PullRequest.HeadRepoID] + } + } + } + return repo_model.ValuesRepository(repoMaps), nil +} + +// LoadRepositories loads issues' all repositories +func (issues IssueList) LoadRepositories() ([]*repo_model.Repository, error) { + return issues.loadRepositories(db.DefaultContext) +} + +func (issues IssueList) getPosterIDs() []int64 { + posterIDs := make(map[int64]struct{}, len(issues)) + for _, issue := range issues { + if _, ok := posterIDs[issue.PosterID]; !ok { + posterIDs[issue.PosterID] = struct{}{} + } + } + return container.KeysInt64(posterIDs) +} + +func (issues IssueList) loadPosters(ctx context.Context) error { + if len(issues) == 0 { + return nil + } + + posterIDs := issues.getPosterIDs() + posterMaps := make(map[int64]*user_model.User, len(posterIDs)) + left := len(posterIDs) + for left > 0 { + limit := db.DefaultMaxInSize + if left < limit { + limit = left + } + err := db.GetEngine(ctx). + In("id", posterIDs[:limit]). + Find(&posterMaps) + if err != nil { + return err + } + left -= limit + posterIDs = posterIDs[limit:] + } + + for _, issue := range issues { + if issue.PosterID <= 0 { + continue + } + var ok bool + if issue.Poster, ok = posterMaps[issue.PosterID]; !ok { + issue.Poster = user_model.NewGhostUser() + } + } + return nil +} + +func (issues IssueList) getIssueIDs() []int64 { + ids := make([]int64, 0, len(issues)) + for _, issue := range issues { + ids = append(ids, issue.ID) + } + return ids +} + +func (issues IssueList) loadLabels(ctx context.Context) error { + if len(issues) == 0 { + return nil + } + + type LabelIssue struct { + Label *Label `xorm:"extends"` + IssueLabel *IssueLabel `xorm:"extends"` + } + + issueLabels := make(map[int64][]*Label, len(issues)*3) + issueIDs := issues.getIssueIDs() + left := len(issueIDs) + for left > 0 { + limit := db.DefaultMaxInSize + if left < limit { + limit = left + } + rows, err := db.GetEngine(ctx).Table("label"). + Join("LEFT", "issue_label", "issue_label.label_id = label.id"). + In("issue_label.issue_id", issueIDs[:limit]). + Asc("label.name"). + Rows(new(LabelIssue)) + if err != nil { + return err + } + + for rows.Next() { + var labelIssue LabelIssue + err = rows.Scan(&labelIssue) + if err != nil { + if err1 := rows.Close(); err1 != nil { + return fmt.Errorf("IssueList.loadLabels: Close: %v", err1) + } + return err + } + issueLabels[labelIssue.IssueLabel.IssueID] = append(issueLabels[labelIssue.IssueLabel.IssueID], labelIssue.Label) + } + // When there are no rows left and we try to close it. + // Since that is not relevant for us, we can safely ignore it. + if err1 := rows.Close(); err1 != nil { + return fmt.Errorf("IssueList.loadLabels: Close: %v", err1) + } + left -= limit + issueIDs = issueIDs[limit:] + } + + for _, issue := range issues { + issue.Labels = issueLabels[issue.ID] + } + return nil +} + +func (issues IssueList) getMilestoneIDs() []int64 { + ids := make(map[int64]struct{}, len(issues)) + for _, issue := range issues { + if _, ok := ids[issue.MilestoneID]; !ok { + ids[issue.MilestoneID] = struct{}{} + } + } + return container.KeysInt64(ids) +} + +func (issues IssueList) loadMilestones(ctx context.Context) error { + milestoneIDs := issues.getMilestoneIDs() + if len(milestoneIDs) == 0 { + return nil + } + + milestoneMaps := make(map[int64]*Milestone, len(milestoneIDs)) + left := len(milestoneIDs) + for left > 0 { + limit := db.DefaultMaxInSize + if left < limit { + limit = left + } + err := db.GetEngine(ctx). + In("id", milestoneIDs[:limit]). + Find(&milestoneMaps) + if err != nil { + return err + } + left -= limit + milestoneIDs = milestoneIDs[limit:] + } + + for _, issue := range issues { + issue.Milestone = milestoneMaps[issue.MilestoneID] + } + return nil +} + +func (issues IssueList) loadAssignees(ctx context.Context) error { + if len(issues) == 0 { + return nil + } + + type AssigneeIssue struct { + IssueAssignee *IssueAssignees `xorm:"extends"` + Assignee *user_model.User `xorm:"extends"` + } + + assignees := make(map[int64][]*user_model.User, len(issues)) + issueIDs := issues.getIssueIDs() + left := len(issueIDs) + for left > 0 { + limit := db.DefaultMaxInSize + if left < limit { + limit = left + } + rows, err := db.GetEngine(ctx).Table("issue_assignees"). + Join("INNER", "`user`", "`user`.id = `issue_assignees`.assignee_id"). + In("`issue_assignees`.issue_id", issueIDs[:limit]). + Rows(new(AssigneeIssue)) + if err != nil { + return err + } + + for rows.Next() { + var assigneeIssue AssigneeIssue + err = rows.Scan(&assigneeIssue) + if err != nil { + if err1 := rows.Close(); err1 != nil { + return fmt.Errorf("IssueList.loadAssignees: Close: %v", err1) + } + return err + } + + assignees[assigneeIssue.IssueAssignee.IssueID] = append(assignees[assigneeIssue.IssueAssignee.IssueID], assigneeIssue.Assignee) + } + if err1 := rows.Close(); err1 != nil { + return fmt.Errorf("IssueList.loadAssignees: Close: %v", err1) + } + left -= limit + issueIDs = issueIDs[limit:] + } + + for _, issue := range issues { + issue.Assignees = assignees[issue.ID] + } + return nil +} + +func (issues IssueList) getPullIssueIDs() []int64 { + ids := make([]int64, 0, len(issues)) + for _, issue := range issues { + if issue.IsPull && issue.PullRequest == nil { + ids = append(ids, issue.ID) + } + } + return ids +} + +func (issues IssueList) loadPullRequests(ctx context.Context) error { + issuesIDs := issues.getPullIssueIDs() + if len(issuesIDs) == 0 { + return nil + } + + pullRequestMaps := make(map[int64]*PullRequest, len(issuesIDs)) + left := len(issuesIDs) + for left > 0 { + limit := db.DefaultMaxInSize + if left < limit { + limit = left + } + rows, err := db.GetEngine(ctx). + In("issue_id", issuesIDs[:limit]). + Rows(new(PullRequest)) + if err != nil { + return err + } + + for rows.Next() { + var pr PullRequest + err = rows.Scan(&pr) + if err != nil { + if err1 := rows.Close(); err1 != nil { + return fmt.Errorf("IssueList.loadPullRequests: Close: %v", err1) + } + return err + } + pullRequestMaps[pr.IssueID] = &pr + } + if err1 := rows.Close(); err1 != nil { + return fmt.Errorf("IssueList.loadPullRequests: Close: %v", err1) + } + left -= limit + issuesIDs = issuesIDs[limit:] + } + + for _, issue := range issues { + issue.PullRequest = pullRequestMaps[issue.ID] + } + return nil +} + +func (issues IssueList) loadAttachments(ctx context.Context) (err error) { + if len(issues) == 0 { + return nil + } + + attachments := make(map[int64][]*repo_model.Attachment, len(issues)) + issuesIDs := issues.getIssueIDs() + left := len(issuesIDs) + for left > 0 { + limit := db.DefaultMaxInSize + if left < limit { + limit = left + } + rows, err := db.GetEngine(ctx).Table("attachment"). + Join("INNER", "issue", "issue.id = attachment.issue_id"). + In("issue.id", issuesIDs[:limit]). + Rows(new(repo_model.Attachment)) + if err != nil { + return err + } + + for rows.Next() { + var attachment repo_model.Attachment + err = rows.Scan(&attachment) + if err != nil { + if err1 := rows.Close(); err1 != nil { + return fmt.Errorf("IssueList.loadAttachments: Close: %v", err1) + } + return err + } + attachments[attachment.IssueID] = append(attachments[attachment.IssueID], &attachment) + } + if err1 := rows.Close(); err1 != nil { + return fmt.Errorf("IssueList.loadAttachments: Close: %v", err1) + } + left -= limit + issuesIDs = issuesIDs[limit:] + } + + for _, issue := range issues { + issue.Attachments = attachments[issue.ID] + } + return nil +} + +func (issues IssueList) loadComments(ctx context.Context, cond builder.Cond) (err error) { + if len(issues) == 0 { + return nil + } + + comments := make(map[int64][]*Comment, len(issues)) + issuesIDs := issues.getIssueIDs() + left := len(issuesIDs) + for left > 0 { + limit := db.DefaultMaxInSize + if left < limit { + limit = left + } + rows, err := db.GetEngine(ctx).Table("comment"). + Join("INNER", "issue", "issue.id = comment.issue_id"). + In("issue.id", issuesIDs[:limit]). + Where(cond). + Rows(new(Comment)) + if err != nil { + return err + } + + for rows.Next() { + var comment Comment + err = rows.Scan(&comment) + if err != nil { + if err1 := rows.Close(); err1 != nil { + return fmt.Errorf("IssueList.loadComments: Close: %v", err1) + } + return err + } + comments[comment.IssueID] = append(comments[comment.IssueID], &comment) + } + if err1 := rows.Close(); err1 != nil { + return fmt.Errorf("IssueList.loadComments: Close: %v", err1) + } + left -= limit + issuesIDs = issuesIDs[limit:] + } + + for _, issue := range issues { + issue.Comments = comments[issue.ID] + } + return nil +} + +func (issues IssueList) loadTotalTrackedTimes(ctx context.Context) (err error) { + type totalTimesByIssue struct { + IssueID int64 + Time int64 + } + if len(issues) == 0 { + return nil + } + trackedTimes := make(map[int64]int64, len(issues)) + + ids := make([]int64, 0, len(issues)) + for _, issue := range issues { + if issue.Repo.IsTimetrackerEnabled() { + ids = append(ids, issue.ID) + } + } + + left := len(ids) + for left > 0 { + limit := db.DefaultMaxInSize + if left < limit { + limit = left + } + + // select issue_id, sum(time) from tracked_time where issue_id in () group by issue_id + rows, err := db.GetEngine(ctx).Table("tracked_time"). + Where("deleted = ?", false). + Select("issue_id, sum(time) as time"). + In("issue_id", ids[:limit]). + GroupBy("issue_id"). + Rows(new(totalTimesByIssue)) + if err != nil { + return err + } + + for rows.Next() { + var totalTime totalTimesByIssue + err = rows.Scan(&totalTime) + if err != nil { + if err1 := rows.Close(); err1 != nil { + return fmt.Errorf("IssueList.loadTotalTrackedTimes: Close: %v", err1) + } + return err + } + trackedTimes[totalTime.IssueID] = totalTime.Time + } + if err1 := rows.Close(); err1 != nil { + return fmt.Errorf("IssueList.loadTotalTrackedTimes: Close: %v", err1) + } + left -= limit + ids = ids[limit:] + } + + for _, issue := range issues { + issue.TotalTrackedTime = trackedTimes[issue.ID] + } + return nil +} + +// loadAttributes loads all attributes, expect for attachments and comments +func (issues IssueList) loadAttributes(ctx context.Context) error { + if _, err := issues.loadRepositories(ctx); err != nil { + return fmt.Errorf("issue.loadAttributes: loadRepositories: %v", err) + } + + if err := issues.loadPosters(ctx); err != nil { + return fmt.Errorf("issue.loadAttributes: loadPosters: %v", err) + } + + if err := issues.loadLabels(ctx); err != nil { + return fmt.Errorf("issue.loadAttributes: loadLabels: %v", err) + } + + if err := issues.loadMilestones(ctx); err != nil { + return fmt.Errorf("issue.loadAttributes: loadMilestones: %v", err) + } + + if err := issues.loadAssignees(ctx); err != nil { + return fmt.Errorf("issue.loadAttributes: loadAssignees: %v", err) + } + + if err := issues.loadPullRequests(ctx); err != nil { + return fmt.Errorf("issue.loadAttributes: loadPullRequests: %v", err) + } + + if err := issues.loadTotalTrackedTimes(ctx); err != nil { + return fmt.Errorf("issue.loadAttributes: loadTotalTrackedTimes: %v", err) + } + + return nil +} + +// LoadAttributes loads attributes of the issues, except for attachments and +// comments +func (issues IssueList) LoadAttributes() error { + return issues.loadAttributes(db.DefaultContext) +} + +// LoadAttachments loads attachments +func (issues IssueList) LoadAttachments() error { + return issues.loadAttachments(db.DefaultContext) +} + +// LoadComments loads comments +func (issues IssueList) LoadComments() error { + return issues.loadComments(db.DefaultContext, builder.NewCond()) +} + +// LoadDiscussComments loads discuss comments +func (issues IssueList) LoadDiscussComments() error { + return issues.loadComments(db.DefaultContext, builder.Eq{"comment.type": CommentTypeComment}) +} + +// LoadPullRequests loads pull requests +func (issues IssueList) LoadPullRequests() error { + return issues.loadPullRequests(db.DefaultContext) +} + +// GetApprovalCounts returns a map of issue ID to slice of approval counts +// FIXME: only returns official counts due to double counting of non-official approvals +func (issues IssueList) GetApprovalCounts(ctx context.Context) (map[int64][]*ReviewCount, error) { + rCounts := make([]*ReviewCount, 0, 2*len(issues)) + ids := make([]int64, len(issues)) + for i, issue := range issues { + ids[i] = issue.ID + } + sess := db.GetEngine(ctx).In("issue_id", ids) + err := sess.Select("issue_id, type, count(id) as `count`"). + Where("official = ? AND dismissed = ?", true, false). + GroupBy("issue_id, type"). + OrderBy("issue_id"). + Table("review"). + Find(&rCounts) + if err != nil { + return nil, err + } + + approvalCountMap := make(map[int64][]*ReviewCount, len(issues)) + + for _, c := range rCounts { + approvalCountMap[c.IssueID] = append(approvalCountMap[c.IssueID], c) + } + + return approvalCountMap, nil +} diff --git a/models/issues/issue_list_test.go b/models/issues/issue_list_test.go new file mode 100644 index 0000000000..6b978f9ae6 --- /dev/null +++ b/models/issues/issue_list_test.go @@ -0,0 +1,73 @@ +// 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 ( + "testing" + + issues_model "code.gitea.io/gitea/models/issues" + "code.gitea.io/gitea/models/unittest" + "code.gitea.io/gitea/modules/setting" + + "github.com/stretchr/testify/assert" +) + +func TestIssueList_LoadRepositories(t *testing.T) { + assert.NoError(t, unittest.PrepareTestDatabase()) + + 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() + assert.NoError(t, err) + assert.Len(t, repos, 2) + for _, issue := range issueList { + assert.EqualValues(t, issue.RepoID, issue.Repo.ID) + } +} + +func TestIssueList_LoadAttributes(t *testing.T) { + assert.NoError(t, unittest.PrepareTestDatabase()) + setting.Service.EnableTimetracking = true + 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()) + for _, issue := range issueList { + assert.EqualValues(t, issue.RepoID, issue.Repo.ID) + for _, label := range issue.Labels { + assert.EqualValues(t, issue.RepoID, label.RepoID) + unittest.AssertExistsAndLoadBean(t, &issues_model.IssueLabel{IssueID: issue.ID, LabelID: label.ID}) + } + if issue.PosterID > 0 { + assert.EqualValues(t, issue.PosterID, issue.Poster.ID) + } + if issue.AssigneeID > 0 { + assert.EqualValues(t, issue.AssigneeID, issue.Assignee.ID) + } + if issue.MilestoneID > 0 { + assert.EqualValues(t, issue.MilestoneID, issue.Milestone.ID) + } + if issue.IsPull { + assert.EqualValues(t, issue.ID, issue.PullRequest.IssueID) + } + for _, attachment := range issue.Attachments { + assert.EqualValues(t, issue.ID, attachment.IssueID) + } + for _, comment := range issue.Comments { + assert.EqualValues(t, issue.ID, comment.IssueID) + } + if issue.ID == int64(1) { + assert.Equal(t, int64(400), issue.TotalTrackedTime) + } else if issue.ID == int64(2) { + assert.Equal(t, int64(3682), issue.TotalTrackedTime) + } + } +} diff --git a/models/issues/issue_lock.go b/models/issues/issue_lock.go new file mode 100644 index 0000000000..7b52429ef7 --- /dev/null +++ b/models/issues/issue_lock.go @@ -0,0 +1,65 @@ +// Copyright 2019 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" + user_model "code.gitea.io/gitea/models/user" +) + +// IssueLockOptions defines options for locking and/or unlocking an issue/PR +type IssueLockOptions struct { + Doer *user_model.User + Issue *Issue + Reason string +} + +// LockIssue locks an issue. This would limit commenting abilities to +// users with write access to the repo +func LockIssue(opts *IssueLockOptions) error { + return updateIssueLock(opts, true) +} + +// UnlockIssue unlocks a previously locked issue. +func UnlockIssue(opts *IssueLockOptions) error { + return updateIssueLock(opts, false) +} + +func updateIssueLock(opts *IssueLockOptions, lock bool) error { + if opts.Issue.IsLocked == lock { + return nil + } + + opts.Issue.IsLocked = lock + var commentType CommentType + if opts.Issue.IsLocked { + commentType = CommentTypeLock + } else { + commentType = CommentTypeUnlock + } + + ctx, committer, err := db.TxContext() + if err != nil { + return err + } + defer committer.Close() + + if err := UpdateIssueCols(ctx, opts.Issue, "is_locked"); err != nil { + return err + } + + opt := &CreateCommentOptions{ + Doer: opts.Doer, + Issue: opts.Issue, + Repo: opts.Issue.Repo, + Type: commentType, + Content: opts.Reason, + } + if _, err := CreateCommentCtx(ctx, opt); err != nil { + return err + } + + return committer.Commit() +} diff --git a/models/issues/issue_project.go b/models/issues/issue_project.go new file mode 100644 index 0000000000..5e0a337f7d --- /dev/null +++ b/models/issues/issue_project.go @@ -0,0 +1,179 @@ +// Copyright 2021 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 ( + "context" + "fmt" + + "code.gitea.io/gitea/models/db" + project_model "code.gitea.io/gitea/models/project" + user_model "code.gitea.io/gitea/models/user" +) + +// LoadProject load the project the issue was assigned to +func (i *Issue) LoadProject() (err error) { + return i.loadProject(db.DefaultContext) +} + +func (i *Issue) loadProject(ctx context.Context) (err error) { + if i.Project == nil { + var p project_model.Project + if _, err = db.GetEngine(ctx).Table("project"). + Join("INNER", "project_issue", "project.id=project_issue.project_id"). + Where("project_issue.issue_id = ?", i.ID). + Get(&p); err != nil { + return err + } + i.Project = &p + } + return +} + +// ProjectID return project id if issue was assigned to one +func (i *Issue) ProjectID() int64 { + return i.projectID(db.DefaultContext) +} + +func (i *Issue) projectID(ctx context.Context) int64 { + var ip project_model.ProjectIssue + has, err := db.GetEngine(ctx).Where("issue_id=?", i.ID).Get(&ip) + if err != nil || !has { + return 0 + } + return ip.ProjectID +} + +// ProjectBoardID return project board id if issue was assigned to one +func (i *Issue) ProjectBoardID() int64 { + return i.projectBoardID(db.DefaultContext) +} + +func (i *Issue) projectBoardID(ctx context.Context) int64 { + var ip project_model.ProjectIssue + has, err := db.GetEngine(ctx).Where("issue_id=?", i.ID).Get(&ip) + if err != nil || !has { + return 0 + } + return ip.ProjectBoardID +} + +// LoadIssuesFromBoard load issues assigned to this board +func LoadIssuesFromBoard(b *project_model.Board) (IssueList, error) { + issueList := make([]*Issue, 0, 10) + + if b.ID != 0 { + issues, err := Issues(&IssuesOptions{ + ProjectBoardID: b.ID, + ProjectID: b.ProjectID, + }) + if err != nil { + return nil, err + } + issueList = issues + } + + if b.Default { + issues, err := Issues(&IssuesOptions{ + ProjectBoardID: -1, // Issues without ProjectBoardID + ProjectID: b.ProjectID, + }) + if err != nil { + return nil, err + } + issueList = append(issueList, issues...) + } + + if err := IssueList(issueList).LoadComments(); err != nil { + return nil, err + } + + return issueList, nil +} + +// LoadIssuesFromBoardList load issues assigned to the boards +func LoadIssuesFromBoardList(bs project_model.BoardList) (map[int64]IssueList, error) { + issuesMap := make(map[int64]IssueList, len(bs)) + for i := range bs { + il, err := LoadIssuesFromBoard(bs[i]) + if err != nil { + return nil, err + } + issuesMap[bs[i].ID] = il + } + return issuesMap, nil +} + +// ChangeProjectAssign changes the project associated with an issue +func ChangeProjectAssign(issue *Issue, doer *user_model.User, newProjectID int64) error { + ctx, committer, err := db.TxContext() + if err != nil { + return err + } + defer committer.Close() + + if err := addUpdateIssueProject(ctx, issue, doer, newProjectID); err != nil { + return err + } + + return committer.Commit() +} + +func addUpdateIssueProject(ctx context.Context, issue *Issue, doer *user_model.User, newProjectID int64) error { + oldProjectID := issue.projectID(ctx) + + if _, err := db.GetEngine(ctx).Where("project_issue.issue_id=?", issue.ID).Delete(&project_model.ProjectIssue{}); err != nil { + return err + } + + if err := issue.LoadRepo(ctx); err != nil { + return err + } + + if oldProjectID > 0 || newProjectID > 0 { + if _, err := CreateCommentCtx(ctx, &CreateCommentOptions{ + Type: CommentTypeProject, + Doer: doer, + Repo: issue.Repo, + Issue: issue, + OldProjectID: oldProjectID, + ProjectID: newProjectID, + }); err != nil { + return err + } + } + + return db.Insert(ctx, &project_model.ProjectIssue{ + IssueID: issue.ID, + ProjectID: newProjectID, + }) +} + +// MoveIssueAcrossProjectBoards move a card from one board to another +func MoveIssueAcrossProjectBoards(issue *Issue, board *project_model.Board) error { + ctx, committer, err := db.TxContext() + if err != nil { + return err + } + defer committer.Close() + sess := db.GetEngine(ctx) + + var pis project_model.ProjectIssue + has, err := sess.Where("issue_id=?", issue.ID).Get(&pis) + if err != nil { + return err + } + + if !has { + return fmt.Errorf("issue has to be added to a project first") + } + + pis.ProjectBoardID = board.ID + if _, err := sess.ID(pis.ID).Cols("project_board_id").Update(&pis); err != nil { + return err + } + + return committer.Commit() +} diff --git a/models/issues/issue_test.go b/models/issues/issue_test.go new file mode 100644 index 0000000000..019e578da8 --- /dev/null +++ b/models/issues/issue_test.go @@ -0,0 +1,562 @@ +// 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 ( + "context" + "fmt" + "sort" + "strconv" + "sync" + "testing" + "time" + + "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" + 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" + "xorm.io/builder" +) + +func TestIssue_ReplaceLabels(t *testing.T) { + assert.NoError(t, unittest.PrepareTestDatabase()) + + testSuccess := func(issueID int64, labelIDs []int64) { + 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([]*issues_model.Label, len(labelIDs)) + for i, labelID := range labelIDs { + labels[i] = unittest.AssertExistsAndLoadBean(t, &issues_model.Label{ID: labelID, RepoID: repo.ID}).(*issues_model.Label) + } + 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, &issues_model.IssueLabel{IssueID: issueID, LabelID: labelID}) + } + } + + testSuccess(1, []int64{2}) + testSuccess(1, []int64{1, 2}) + testSuccess(1, []int64{}) +} + +func Test_GetIssueIDsByRepoID(t *testing.T) { + assert.NoError(t, unittest.PrepareTestDatabase()) + + 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, &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()) +} + +func TestGetIssuesByIDs(t *testing.T) { + assert.NoError(t, unittest.PrepareTestDatabase()) + testSuccess := func(expectedIssueIDs, nonExistentIssueIDs []int64) { + issues, err := issues_model.GetIssuesByIDs(db.DefaultContext, append(expectedIssueIDs, nonExistentIssueIDs...)) + assert.NoError(t, err) + actualIssueIDs := make([]int64, len(issues)) + for i, issue := range issues { + actualIssueIDs[i] = issue.ID + } + assert.Equal(t, expectedIssueIDs, actualIssueIDs) + } + testSuccess([]int64{1, 2, 3}, []int64{}) + testSuccess([]int64{1, 2, 3}, []int64{unittest.NonexistentID}) +} + +func TestGetParticipantIDsByIssue(t *testing.T) { + assert.NoError(t, unittest.PrepareTestDatabase()) + + checkParticipants := func(issueID int64, userIDs []int) { + issue, err := issues_model.GetIssueByID(db.DefaultContext, issueID) + assert.NoError(t, err) + participants, err := issue.GetParticipantIDsByIssue(db.DefaultContext) + if assert.NoError(t, err) { + participantsIDs := make([]int, len(participants)) + for i, uid := range participants { + participantsIDs[i] = int(uid) + } + sort.Ints(participantsIDs) + sort.Ints(userIDs) + assert.Equal(t, userIDs, participantsIDs) + } + } + + // User 1 is issue1 poster (see fixtures/issue.yml) + // User 2 only labeled issue1 (see fixtures/comment.yml) + // Users 3 and 5 made actual comments (see fixtures/comment.yml) + // User 3 is inactive, thus not active participant + checkParticipants(1, []int{1, 5}) +} + +func TestIssue_ClearLabels(t *testing.T) { + tests := []struct { + issueID int64 + doerID int64 + }{ + {1, 2}, // non-pull-request, has labels + {2, 2}, // pull-request, has labels + {3, 2}, // pull-request, has no labels + } + for _, test := range tests { + assert.NoError(t, unittest.PrepareTestDatabase()) + 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, 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, &issues_model.Issue{}).(*issues_model.Issue) + + const newTitle = "New Title for unit test" + issue.Title = newTitle + + prevContent := issue.Content + issue.Content = "This should have no effect" + + now := time.Now().Unix() + assert.NoError(t, issues_model.UpdateIssueCols(db.DefaultContext, issue, "name")) + then := time.Now().Unix() + + 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)) +} + +func TestIssues(t *testing.T) { + assert.NoError(t, unittest.PrepareTestDatabase()) + for _, test := range []struct { + Opts issues_model.IssuesOptions + ExpectedIssueIDs []int64 + }{ + { + issues_model.IssuesOptions{ + AssigneeID: 1, + SortType: "oldest", + }, + []int64{1, 6}, + }, + { + issues_model.IssuesOptions{ + RepoCond: builder.In("repo_id", 1, 3), + SortType: "oldest", + ListOptions: db.ListOptions{ + Page: 1, + PageSize: 4, + }, + }, + []int64{1, 2, 3, 5}, + }, + { + issues_model.IssuesOptions{ + LabelIDs: []int64{1}, + ListOptions: db.ListOptions{ + Page: 1, + PageSize: 4, + }, + }, + []int64{2, 1}, + }, + { + issues_model.IssuesOptions{ + LabelIDs: []int64{1, 2}, + ListOptions: db.ListOptions{ + Page: 1, + PageSize: 4, + }, + }, + []int64{}, // issues with **both** label 1 and 2, none of these issues matches, TODO: add more tests + }, + } { + issues, err := issues_model.Issues(&test.Opts) + assert.NoError(t, err) + if assert.Len(t, issues, len(test.ExpectedIssueIDs)) { + for i, issue := range issues { + assert.EqualValues(t, test.ExpectedIssueIDs[i], issue.ID) + } + } + } +} + +func TestGetUserIssueStats(t *testing.T) { + assert.NoError(t, unittest.PrepareTestDatabase()) + for _, test := range []struct { + Opts issues_model.UserIssueStatsOptions + ExpectedIssueStats issues_model.IssueStats + }{ + { + issues_model.UserIssueStatsOptions{ + UserID: 1, + RepoIDs: []int64{1}, + FilterMode: issues_model.FilterModeAll, + }, + issues_model.IssueStats{ + YourRepositoriesCount: 1, // 6 + AssignCount: 1, // 6 + CreateCount: 1, // 6 + OpenCount: 1, // 6 + ClosedCount: 1, // 1 + }, + }, + { + issues_model.UserIssueStatsOptions{ + UserID: 1, + RepoIDs: []int64{1}, + FilterMode: issues_model.FilterModeAll, + IsClosed: true, + }, + issues_model.IssueStats{ + YourRepositoriesCount: 1, // 6 + AssignCount: 0, + CreateCount: 0, + OpenCount: 1, // 6 + ClosedCount: 1, // 1 + }, + }, + { + issues_model.UserIssueStatsOptions{ + UserID: 1, + FilterMode: issues_model.FilterModeAssign, + }, + issues_model.IssueStats{ + YourRepositoriesCount: 1, // 6 + AssignCount: 1, // 6 + CreateCount: 1, // 6 + OpenCount: 1, // 6 + ClosedCount: 0, + }, + }, + { + issues_model.UserIssueStatsOptions{ + UserID: 1, + FilterMode: issues_model.FilterModeCreate, + }, + issues_model.IssueStats{ + YourRepositoriesCount: 1, // 6 + AssignCount: 1, // 6 + CreateCount: 1, // 6 + OpenCount: 1, // 6 + ClosedCount: 0, + }, + }, + { + issues_model.UserIssueStatsOptions{ + UserID: 1, + FilterMode: issues_model.FilterModeMention, + }, + issues_model.IssueStats{ + YourRepositoriesCount: 1, // 6 + AssignCount: 1, // 6 + CreateCount: 1, // 6 + MentionCount: 0, + OpenCount: 0, + ClosedCount: 0, + }, + }, + { + issues_model.UserIssueStatsOptions{ + UserID: 1, + FilterMode: issues_model.FilterModeCreate, + IssueIDs: []int64{1}, + }, + issues_model.IssueStats{ + YourRepositoriesCount: 1, // 1 + AssignCount: 1, // 1 + CreateCount: 1, // 1 + OpenCount: 1, // 1 + ClosedCount: 0, + }, + }, + { + 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: issues_model.FilterModeAll, + }, + issues_model.IssueStats{ + YourRepositoriesCount: 2, + AssignCount: 1, + CreateCount: 1, + OpenCount: 2, + }, + }, + } { + t.Run(fmt.Sprintf("%#v", test.Opts), func(t *testing.T) { + stats, err := issues_model.GetUserIssueStats(test.Opts) + if !assert.NoError(t, err) { + return + } + assert.Equal(t, test.ExpectedIssueStats, *stats) + }) + } +} + +func TestIssue_loadTotalTimes(t *testing.T) { + assert.NoError(t, unittest.PrepareTestDatabase()) + ms, err := issues_model.GetIssueByID(db.DefaultContext, 2) + assert.NoError(t, err) + 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 := 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 = 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 = 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 = 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) +} + +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 issues_model.IssuesOptions + ExpectedRepoIDs []int64 + }{ + { + issues_model.IssuesOptions{ + AssigneeID: 2, + }, + []int64{3, 32}, + }, + { + issues_model.IssuesOptions{ + RepoCond: builder.In("repo_id", 1, 2), + }, + []int64{1, 2}, + }, + } { + 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 { + assert.EqualValues(t, test.ExpectedRepoIDs[i], repoID) + } + } + } +} + +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 := issues_model.Issue{ + RepoID: repo.ID, + PosterID: user.ID, + Poster: user, + Title: title, + Content: content, + } + err := issues_model.NewIssue(repo, &issue, nil, nil) + assert.NoError(t, err) + + has, err := db.GetEngine(db.DefaultContext).ID(issue.ID).Get(&newIssue) + assert.NoError(t, err) + assert.True(t, has) + assert.EqualValues(t, issue.Title, newIssue.Title) + assert.EqualValues(t, issue.Content, newIssue.Content) + if expectIndex > 0 { + assert.EqualValues(t, expectIndex, newIssue.Index) + } + }) + return &newIssue +} + +func TestIssue_InsertIssue(t *testing.T) { + assert.NoError(t, unittest.PrepareTestDatabase()) + + // 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(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(issues_model.Issue)) + assert.NoError(t, err) +} + +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 := &issues_model.Issue{RepoID: r.ID} + d := unittest.AssertExistsAndLoadBean(t, &user_model.User{LowerName: doer}).(*user_model.User) + resolved, err := issues_model.ResolveIssueMentionsByVisibility(db.DefaultContext, issue, d, mentions) + assert.NoError(t, err) + ids := make([]int64, len(resolved)) + for i, user := range resolved { + ids[i] = user.ID + } + sort.Slice(ids, func(i, j int) bool { return ids[i] < ids[j] }) + assert.EqualValues(t, expected, ids) + } + + // Public repo, existing user + testSuccess("user2", "repo1", "user1", []string{"user5"}, []int64{5}) + // Public repo, non-existing user + testSuccess("user2", "repo1", "user1", []string{"nonexisting"}, []int64{}) + // Public repo, doer + testSuccess("user2", "repo1", "user1", []string{"user1"}, []int64{}) + // Private repo, team member + testSuccess("user17", "big_test_private_4", "user20", []string{"user2"}, []int64{2}) + // Private repo, not a team member + testSuccess("user17", "big_test_private_4", "user20", []string{"user5"}, []int64{}) + // Private repo, whole team + testSuccess("user17", "big_test_private_4", "user15", []string{"user17/owners"}, []int64{18}) +} + +func TestResourceIndex(t *testing.T) { + assert.NoError(t, unittest.PrepareTestDatabase()) + + var wg sync.WaitGroup + for i := 0; i < 100; i++ { + wg.Add(1) + go func(i int) { + testInsertIssue(t, fmt.Sprintf("issue %d", i+1), "my issue", 0) + wg.Done() + }(i) + } + wg.Wait() +} + +func TestCorrectIssueStats(t *testing.T) { + assert.NoError(t, unittest.PrepareTestDatabase()) + + // Because the condition is to have chunked database look-ups, + // We have to more issues than `maxQueryParameters`, we will insert. + // maxQueryParameters + 10 issues into the testDatabase. + // Each new issues will have a constant description "Bugs are nasty" + // Which will be used later on. + + issueAmount := issues_model.MaxQueryParameters + 10 + + var wg sync.WaitGroup + for i := 0; i < issueAmount; i++ { + wg.Add(1) + go func(i int) { + testInsertIssue(t, fmt.Sprintf("Issue %d", i+1), "Bugs are nasty", 0) + wg.Done() + }(i) + } + wg.Wait() + + // Now we will get all issueID's that match the "Bugs are nasty" query. + total, ids, err := issues_model.SearchIssueIDsByKeyword(context.TODO(), "Bugs are nasty", []int64{1}, issueAmount, 0) + + // Just to be sure. + assert.NoError(t, err) + assert.EqualValues(t, issueAmount, total) + + // Now we will call the GetIssueStats with these IDs and if working, + // get the correct stats back. + issueStats, err := issues_model.GetIssueStats(&issues_model.IssueStatsOptions{ + RepoID: 1, + IssueIDs: ids, + }) + + // Now check the values. + assert.NoError(t, err) + assert.EqualValues(t, issueStats.OpenCount, issueAmount) +} + +func TestIssueForeignReference(t *testing.T) { + assert.NoError(t, unittest.PrepareTestDatabase()) + 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(db.DefaultContext) + assert.NoError(t, err) + assert.Nil(t, issue.ForeignReference) + + var foreignIndex int64 = 12345 + _, err = issues_model.GetIssueByForeignIndex(context.Background(), issue.RepoID, foreignIndex) + assert.True(t, foreignreference.IsErrLocalIndexNotExist(err)) + + err = db.Insert(db.DefaultContext, &foreignreference.ForeignReference{ + LocalIndex: issue.Index, + ForeignIndex: strconv.FormatInt(foreignIndex, 10), + RepoID: issue.RepoID, + Type: foreignreference.TypeIssue, + }) + assert.NoError(t, err) + + err = issue.LoadAttributes(db.DefaultContext) + assert.NoError(t, err) + + assert.EqualValues(t, issue.ForeignReference.ForeignIndex, strconv.FormatInt(foreignIndex, 10)) + + found, err := issues_model.GetIssueByForeignIndex(context.Background(), issue.RepoID, foreignIndex) + assert.NoError(t, err) + assert.EqualValues(t, found.Index, issue.Index) +} + +func TestMilestoneList_LoadTotalTrackedTimes(t *testing.T) { + assert.NoError(t, unittest.PrepareTestDatabase()) + miles := issues_model.MilestoneList{ + unittest.AssertExistsAndLoadBean(t, &issues_model.Milestone{ID: 1}).(*issues_model.Milestone), + } + + assert.NoError(t, miles.LoadTotalTrackedTimes()) + + assert.Equal(t, int64(3682), miles[0].TotalTrackedTime) +} + +func TestLoadTotalTrackedTime(t *testing.T) { + assert.NoError(t, unittest.PrepareTestDatabase()) + milestone := unittest.AssertExistsAndLoadBean(t, &issues_model.Milestone{ID: 1}).(*issues_model.Milestone) + + assert.NoError(t, milestone.LoadTotalTrackedTime()) + + assert.Equal(t, int64(3682), milestone.TotalTrackedTime) +} + +func TestCountIssues(t *testing.T) { + assert.NoError(t, unittest.PrepareTestDatabase()) + count, err := issues_model.CountIssues(&issues_model.IssuesOptions{}) + assert.NoError(t, err) + assert.EqualValues(t, 17, count) +} diff --git a/models/issues/issue_user.go b/models/issues/issue_user.go new file mode 100644 index 0000000000..f5d22589af --- /dev/null +++ b/models/issues/issue_user.go @@ -0,0 +1,87 @@ +// 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 ( + "context" + "fmt" + + "code.gitea.io/gitea/models/db" + repo_model "code.gitea.io/gitea/models/repo" +) + +// IssueUser represents an issue-user relation. +type IssueUser struct { + ID int64 `xorm:"pk autoincr"` + UID int64 `xorm:"INDEX"` // User ID. + IssueID int64 + IsRead bool + IsMentioned bool +} + +func init() { + db.RegisterModel(new(IssueUser)) +} + +// 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) + } + + // Poster can be anyone, append later if not one of assignees. + isPosterAssignee := false + + // Leave a seat for poster itself to append later, but if poster is one of assignee + // and just waste 1 unit is cheaper than re-allocate memory once. + issueUsers := make([]*IssueUser, 0, len(assignees)+1) + for _, assignee := range assignees { + issueUsers = append(issueUsers, &IssueUser{ + IssueID: issue.ID, + UID: assignee.ID, + }) + isPosterAssignee = isPosterAssignee || assignee.ID == issue.PosterID + } + if !isPosterAssignee { + issueUsers = append(issueUsers, &IssueUser{ + IssueID: issue.ID, + UID: issue.PosterID, + }) + } + + return db.Insert(ctx, issueUsers) +} + +// UpdateIssueUserByRead updates issue-user relation for reading. +func UpdateIssueUserByRead(uid, issueID int64) error { + _, err := db.GetEngine(db.DefaultContext).Exec("UPDATE `issue_user` SET is_read=? WHERE uid=? AND issue_id=?", true, uid, issueID) + return err +} + +// UpdateIssueUsersByMentions updates issue-user pairs by mentioning. +func UpdateIssueUsersByMentions(ctx context.Context, issueID int64, uids []int64) error { + for _, uid := range uids { + iu := &IssueUser{ + UID: uid, + IssueID: issueID, + } + has, err := db.GetEngine(ctx).Get(iu) + if err != nil { + return err + } + + iu.IsMentioned = true + if has { + _, err = db.GetEngine(ctx).ID(iu.ID).Cols("is_mentioned").Update(iu) + } else { + _, err = db.GetEngine(ctx).Insert(iu) + } + if err != nil { + return err + } + } + return nil +} 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/issues/issue_watch.go b/models/issues/issue_watch.go new file mode 100644 index 0000000000..bf907aa8fd --- /dev/null +++ b/models/issues/issue_watch.go @@ -0,0 +1,135 @@ +// 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 ( + "context" + + "code.gitea.io/gitea/models/db" + repo_model "code.gitea.io/gitea/models/repo" + user_model "code.gitea.io/gitea/models/user" + "code.gitea.io/gitea/modules/timeutil" +) + +// IssueWatch is connection request for receiving issue notification. +type IssueWatch struct { + ID int64 `xorm:"pk autoincr"` + UserID int64 `xorm:"UNIQUE(watch) NOT NULL"` + IssueID int64 `xorm:"UNIQUE(watch) NOT NULL"` + IsWatching bool `xorm:"NOT NULL"` + CreatedUnix timeutil.TimeStamp `xorm:"created NOT NULL"` + UpdatedUnix timeutil.TimeStamp `xorm:"updated NOT NULL"` +} + +func init() { + db.RegisterModel(new(IssueWatch)) +} + +// IssueWatchList contains IssueWatch +type IssueWatchList []*IssueWatch + +// CreateOrUpdateIssueWatch set watching for a user and issue +func CreateOrUpdateIssueWatch(userID, issueID int64, isWatching bool) error { + iw, exists, err := GetIssueWatch(db.DefaultContext, userID, issueID) + if err != nil { + return err + } + + if !exists { + iw = &IssueWatch{ + UserID: userID, + IssueID: issueID, + IsWatching: isWatching, + } + + if _, err := db.GetEngine(db.DefaultContext).Insert(iw); err != nil { + return err + } + } else { + iw.IsWatching = isWatching + + if _, err := db.GetEngine(db.DefaultContext).ID(iw.ID).Cols("is_watching", "updated_unix").Update(iw); err != nil { + return err + } + } + return nil +} + +// GetIssueWatch returns all IssueWatch objects from db by user and issue +// the current Web-UI need iw object for watchers AND explicit non-watchers +func GetIssueWatch(ctx context.Context, userID, issueID int64) (iw *IssueWatch, exists bool, err error) { + iw = new(IssueWatch) + exists, err = db.GetEngine(ctx). + Where("user_id = ?", userID). + And("issue_id = ?", issueID). + Get(iw) + return +} + +// CheckIssueWatch check if an user is watching an issue +// it takes participants and repo watch into account +func CheckIssueWatch(user *user_model.User, issue *Issue) (bool, error) { + iw, exist, err := GetIssueWatch(db.DefaultContext, user.ID, issue.ID) + if err != nil { + return false, err + } + if exist { + return iw.IsWatching, nil + } + w, err := repo_model.GetWatch(db.DefaultContext, user.ID, issue.RepoID) + if err != nil { + return false, err + } + return repo_model.IsWatchMode(w.Mode) || IsUserParticipantsOfIssue(user, issue), nil +} + +// GetIssueWatchersIDs returns IDs of subscribers or explicit unsubscribers to a given issue id +// but avoids joining with `user` for performance reasons +// User permissions must be verified elsewhere if required +func GetIssueWatchersIDs(ctx context.Context, issueID int64, watching bool) ([]int64, error) { + ids := make([]int64, 0, 64) + return ids, db.GetEngine(ctx).Table("issue_watch"). + Where("issue_id=?", issueID). + And("is_watching = ?", watching). + Select("user_id"). + Find(&ids) +} + +// GetIssueWatchers returns watchers/unwatchers of a given issue +func GetIssueWatchers(ctx context.Context, issueID int64, listOptions db.ListOptions) (IssueWatchList, error) { + sess := db.GetEngine(ctx). + Where("`issue_watch`.issue_id = ?", issueID). + And("`issue_watch`.is_watching = ?", true). + And("`user`.is_active = ?", true). + And("`user`.prohibit_login = ?", false). + Join("INNER", "`user`", "`user`.id = `issue_watch`.user_id") + + if listOptions.Page != 0 { + sess = db.SetSessionPagination(sess, &listOptions) + watches := make([]*IssueWatch, 0, listOptions.PageSize) + return watches, sess.Find(&watches) + } + watches := make([]*IssueWatch, 0, 8) + return watches, sess.Find(&watches) +} + +// CountIssueWatchers count watchers/unwatchers of a given issue +func CountIssueWatchers(ctx context.Context, issueID int64) (int64, error) { + return db.GetEngine(ctx). + Where("`issue_watch`.issue_id = ?", issueID). + And("`issue_watch`.is_watching = ?", true). + And("`user`.is_active = ?", true). + And("`user`.prohibit_login = ?", false). + Join("INNER", "`user`", "`user`.id = `issue_watch`.user_id").Count(new(IssueWatch)) +} + +// 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). + Delete(new(IssueWatch)) + return err +} diff --git a/models/issues/issue_watch_test.go b/models/issues/issue_watch_test.go new file mode 100644 index 0000000000..c6b6416d9b --- /dev/null +++ b/models/issues/issue_watch_test.go @@ -0,0 +1,68 @@ +// 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 ( + "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" +) + +func TestCreateOrUpdateIssueWatch(t *testing.T) { + assert.NoError(t, unittest.PrepareTestDatabase()) + + 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, 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 := issues_model.GetIssueWatch(db.DefaultContext, 9, 1) + assert.True(t, exists) + assert.NoError(t, err) + + 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 = issues_model.GetIssueWatch(db.DefaultContext, 3, 1) + assert.False(t, exists) + assert.NoError(t, err) +} + +func TestGetIssueWatchers(t *testing.T) { + assert.NoError(t, unittest.PrepareTestDatabase()) + + 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 = issues_model.GetIssueWatchers(db.DefaultContext, 2, db.ListOptions{}) + assert.NoError(t, err) + // Watcher is explicit not watching + assert.Len(t, iws, 0) + + 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 = 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/issues/issue_xref.go b/models/issues/issue_xref.go new file mode 100644 index 0000000000..f4380a02ec --- /dev/null +++ b/models/issues/issue_xref.go @@ -0,0 +1,357 @@ +// Copyright 2019 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 ( + "context" + "fmt" + + "code.gitea.io/gitea/models/db" + access_model "code.gitea.io/gitea/models/perm/access" + repo_model "code.gitea.io/gitea/models/repo" + user_model "code.gitea.io/gitea/models/user" + "code.gitea.io/gitea/modules/log" + "code.gitea.io/gitea/modules/references" +) + +type crossReference struct { + Issue *Issue + Action references.XRefAction +} + +// crossReferencesContext is context to pass along findCrossReference functions +type crossReferencesContext struct { + Type CommentType + Doer *user_model.User + OrigIssue *Issue + OrigComment *Comment + RemoveOld bool +} + +func findOldCrossReferences(ctx context.Context, issueID, commentID int64) ([]*Comment, error) { + active := make([]*Comment, 0, 10) + return active, db.GetEngine(ctx).Where("`ref_action` IN (?, ?, ?)", references.XRefActionNone, references.XRefActionCloses, references.XRefActionReopens). + And("`ref_issue_id` = ?", issueID). + And("`ref_comment_id` = ?", commentID). + Find(&active) +} + +func neuterCrossReferences(ctx context.Context, issueID, commentID int64) error { + active, err := findOldCrossReferences(ctx, issueID, commentID) + if err != nil { + return err + } + ids := make([]int64, len(active)) + for i, c := range active { + ids[i] = c.ID + } + return neuterCrossReferencesIds(ctx, ids) +} + +func neuterCrossReferencesIds(ctx context.Context, ids []int64) error { + _, err := db.GetEngine(ctx).In("id", ids).Cols("`ref_action`").Update(&Comment{RefAction: references.XRefActionNeutered}) + return err +} + +// 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 + } else { + commentType = CommentTypeIssueRef + } + ctx := &crossReferencesContext{ + Type: commentType, + Doer: doer, + OrigIssue: issue, + RemoveOld: removeOld, + } + return issue.createCrossReferences(stdCtx, ctx, issue.Title, issue.Content) +} + +func (issue *Issue) createCrossReferences(stdCtx context.Context, ctx *crossReferencesContext, plaincontent, mdcontent string) error { + xreflist, err := ctx.OrigIssue.getCrossReferences(stdCtx, ctx, plaincontent, mdcontent) + if err != nil { + return err + } + if ctx.RemoveOld { + var commentID int64 + if ctx.OrigComment != nil { + commentID = ctx.OrigComment.ID + } + active, err := findOldCrossReferences(stdCtx, ctx.OrigIssue.ID, commentID) + if err != nil { + return err + } + ids := make([]int64, 0, len(active)) + for _, c := range active { + found := false + for i, x := range xreflist { + if x.Issue.ID == c.IssueID && x.Action == c.RefAction { + found = true + xreflist = append(xreflist[:i], xreflist[i+1:]...) + break + } + } + if !found { + ids = append(ids, c.ID) + } + } + if len(ids) > 0 { + if err = neuterCrossReferencesIds(stdCtx, ids); err != nil { + return err + } + } + } + for _, xref := range xreflist { + var refCommentID int64 + if ctx.OrigComment != nil { + refCommentID = ctx.OrigComment.ID + } + opts := &CreateCommentOptions{ + Type: ctx.Type, + Doer: ctx.Doer, + Repo: xref.Issue.Repo, + Issue: xref.Issue, + RefRepoID: ctx.OrigIssue.RepoID, + RefIssueID: ctx.OrigIssue.ID, + RefCommentID: refCommentID, + RefAction: xref.Action, + RefIsPull: ctx.OrigIssue.IsPull, + } + _, err := CreateCommentCtx(stdCtx, opts) + if err != nil { + return err + } + } + return nil +} + +func (issue *Issue) getCrossReferences(stdCtx context.Context, ctx *crossReferencesContext, plaincontent, mdcontent string) ([]*crossReference, error) { + xreflist := make([]*crossReference, 0, 5) + var ( + refRepo *repo_model.Repository + refIssue *Issue + refAction references.XRefAction + err error + ) + + allrefs := append(references.FindAllIssueReferences(plaincontent), references.FindAllIssueReferencesMarkdown(mdcontent)...) + for _, ref := range allrefs { + if ref.Owner == "" && ref.Name == "" { + // Issues in the same repository + if err := ctx.OrigIssue.LoadRepo(stdCtx); err != nil { + return nil, err + } + refRepo = ctx.OrigIssue.Repo + } else { + // Issues in other repositories + refRepo, err = repo_model.GetRepositoryByOwnerAndNameCtx(stdCtx, ref.Owner, ref.Name) + if err != nil { + if repo_model.IsErrRepoNotExist(err) { + continue + } + return nil, err + } + } + if refIssue, refAction, err = ctx.OrigIssue.verifyReferencedIssue(stdCtx, ctx, refRepo, ref); err != nil { + return nil, err + } + if refIssue != nil { + xreflist = ctx.OrigIssue.updateCrossReferenceList(xreflist, &crossReference{ + Issue: refIssue, + Action: refAction, + }) + } + } + + return xreflist, nil +} + +func (issue *Issue) updateCrossReferenceList(list []*crossReference, xref *crossReference) []*crossReference { + if xref.Issue.ID == issue.ID { + return list + } + for i, r := range list { + if r.Issue.ID == xref.Issue.ID { + if xref.Action != references.XRefActionNone { + list[i].Action = xref.Action + } + return list + } + } + return append(list, xref) +} + +// verifyReferencedIssue will check if the referenced issue exists, and whether the doer has permission to do what +func (issue *Issue) verifyReferencedIssue(stdCtx context.Context, ctx *crossReferencesContext, repo *repo_model.Repository, + ref references.IssueReference, +) (*Issue, references.XRefAction, error) { + refIssue := &Issue{RepoID: repo.ID, Index: ref.Index} + refAction := ref.Action + e := db.GetEngine(stdCtx) + + if has, _ := e.Get(refIssue); !has { + return nil, references.XRefActionNone, nil + } + if err := refIssue.LoadRepo(stdCtx); err != nil { + return nil, references.XRefActionNone, err + } + + // Close/reopen actions can only be set from pull requests to issues + if refIssue.IsPull || !issue.IsPull { + refAction = references.XRefActionNone + } + + // Check doer permissions; set action to None if the doer can't change the destination + if refIssue.RepoID != ctx.OrigIssue.RepoID || ref.Action != references.XRefActionNone { + perm, err := access_model.GetUserRepoPermission(stdCtx, refIssue.Repo, ctx.Doer) + if err != nil { + return nil, references.XRefActionNone, err + } + if !perm.CanReadIssuesOrPulls(refIssue.IsPull) { + return nil, references.XRefActionNone, nil + } + // Accept close/reopening actions only if the poster is able to close the + // referenced issue manually at this moment. The only exception is + // the poster of a new PR referencing an issue on the same repo: then the merger + // should be responsible for checking whether the reference should resolve. + if ref.Action != references.XRefActionNone && + ctx.Doer.ID != refIssue.PosterID && + !perm.CanWriteIssuesOrPulls(refIssue.IsPull) && + (refIssue.RepoID != ctx.OrigIssue.RepoID || ctx.OrigComment != nil) { + refAction = references.XRefActionNone + } + } + + return refIssue, refAction, nil +} + +// 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 + } + if err := comment.LoadIssueCtx(stdCtx); err != nil { + return err + } + ctx := &crossReferencesContext{ + Type: CommentTypeCommentRef, + Doer: doer, + OrigIssue: comment.Issue, + OrigComment: comment, + RemoveOld: removeOld, + } + return comment.Issue.createCrossReferences(stdCtx, ctx, "", comment.Content) +} + +func (comment *Comment) neuterCrossReferences(ctx context.Context) error { + return neuterCrossReferences(ctx, comment.IssueID, comment.ID) +} + +// LoadRefComment loads comment that created this reference from database +func (comment *Comment) LoadRefComment() (err error) { + if comment.RefComment != nil { + return nil + } + comment.RefComment, err = GetCommentByID(db.DefaultContext, comment.RefCommentID) + return +} + +// LoadRefIssue loads comment that created this reference from database +func (comment *Comment) LoadRefIssue() (err error) { + if comment.RefIssue != nil { + return nil + } + comment.RefIssue, err = GetIssueByID(db.DefaultContext, comment.RefIssueID) + if err == nil { + err = comment.RefIssue.LoadRepo(db.DefaultContext) + } + return +} + +// CommentTypeIsRef returns true if CommentType is a reference from another issue +func CommentTypeIsRef(t CommentType) bool { + return t == CommentTypeCommentRef || t == CommentTypePullRef || t == CommentTypeIssueRef +} + +// RefCommentHTMLURL returns the HTML URL for the comment that created this reference +func (comment *Comment) RefCommentHTMLURL() string { + // Edge case for when the reference is inside the title or the description of the referring issue + if comment.RefCommentID == 0 { + return comment.RefIssueHTMLURL() + } + if err := comment.LoadRefComment(); err != nil { // Silently dropping errors :unamused: + log.Error("LoadRefComment(%d): %v", comment.RefCommentID, err) + return "" + } + return comment.RefComment.HTMLURL() +} + +// RefIssueHTMLURL returns the HTML URL of the issue where this reference was created +func (comment *Comment) RefIssueHTMLURL() string { + if err := comment.LoadRefIssue(); err != nil { // Silently dropping errors :unamused: + log.Error("LoadRefIssue(%d): %v", comment.RefCommentID, err) + return "" + } + return comment.RefIssue.HTMLURL() +} + +// RefIssueTitle returns the title of the issue where this reference was created +func (comment *Comment) RefIssueTitle() string { + if err := comment.LoadRefIssue(); err != nil { // Silently dropping errors :unamused: + log.Error("LoadRefIssue(%d): %v", comment.RefCommentID, err) + return "" + } + return comment.RefIssue.Title +} + +// RefIssueIdent returns the user friendly identity (e.g. "#1234") of the issue where this reference was created +func (comment *Comment) RefIssueIdent() string { + if err := comment.LoadRefIssue(); err != nil { // Silently dropping errors :unamused: + log.Error("LoadRefIssue(%d): %v", comment.RefCommentID, err) + return "" + } + // FIXME: check this name for cross-repository references (#7901 if it gets merged) + return fmt.Sprintf("#%d", comment.RefIssue.Index) +} + +// __________ .__ .__ __________ __ +// \______ \__ __| | | |\______ \ ____ ________ __ ____ _______/ |_ +// | ___/ | \ | | | | _// __ \/ ____/ | \_/ __ \ / ___/\ __\ +// | | | | / |_| |_| | \ ___< <_| | | /\ ___/ \___ \ | | +// |____| |____/|____/____/____|_ /\___ >__ |____/ \___ >____ > |__| +// \/ \/ |__| \/ \/ + +// ResolveCrossReferences will return the list of references to close/reopen by this PR +func (pr *PullRequest) ResolveCrossReferences(ctx context.Context) ([]*Comment, error) { + unfiltered := make([]*Comment, 0, 5) + if err := db.GetEngine(ctx). + Where("ref_repo_id = ? AND ref_issue_id = ?", pr.Issue.RepoID, pr.Issue.ID). + In("ref_action", []references.XRefAction{references.XRefActionCloses, references.XRefActionReopens}). + OrderBy("id"). + Find(&unfiltered); err != nil { + return nil, fmt.Errorf("get reference: %v", err) + } + + refs := make([]*Comment, 0, len(unfiltered)) + for _, ref := range unfiltered { + found := false + for i, r := range refs { + if r.IssueID == ref.IssueID { + // Keep only the latest + refs[i] = ref + found = true + break + } + } + if !found { + refs = append(refs, ref) + } + } + + return refs, nil +} diff --git a/models/issues/issue_xref_test.go b/models/issues/issue_xref_test.go new file mode 100644 index 0000000000..6bb19d5328 --- /dev/null +++ b/models/issues/issue_xref_test.go @@ -0,0 +1,184 @@ +// Copyright 2019 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 ( + "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" + "code.gitea.io/gitea/modules/references" + + "github.com/stretchr/testify/assert" +) + +func TestXRef_AddCrossReferences(t *testing.T) { + assert.NoError(t, unittest.PrepareTestDatabase()) + + // Issue #1 to test against + itarget := testCreateIssue(t, 1, 2, "title1", "content1", false) + + // 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, &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) + + // 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, &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) + + // Issue mentioning issue #1 + content = fmt.Sprintf("content3, mentions #%d", itarget.Index) + i := testCreateIssue(t, 1, 2, "title3", content, false) + 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) + + // Issue #4 to test against + itarget = testCreateIssue(t, 3, 3, "title4", "content4", false) + + // 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, &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) + + // 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, &issues_model.Comment{IssueID: itarget.ID, RefIssueID: i.ID, RefCommentID: 0}) +} + +func TestXRef_NeuterCrossReferences(t *testing.T) { + assert.NoError(t, unittest.PrepareTestDatabase()) + + // Issue #1 to test against + itarget := testCreateIssue(t, 1, 2, "title1", "content1", false) + + // Issue mentioning issue #1 + title := fmt.Sprintf("title2, mentions #%d", itarget.Index) + i := testCreateIssue(t, 1, 2, title, "content2", false) + 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, issues_model.ChangeIssueTitle(i, d, title)) + + 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) +} + +func TestXRef_ResolveCrossReferences(t *testing.T) { + assert.NoError(t, unittest.PrepareTestDatabase()) + + d := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2}).(*user_model.User) + + 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 := 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, &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, &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, &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, &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, &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) + assert.Len(t, refs, 3) + assert.Equal(t, rp.ID, refs[0].ID, "bad ref rp: %+v", refs[0]) + assert.Equal(t, r1.ID, refs[1].ID, "bad ref r1: %+v", refs[1]) + 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) *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 := &issues_model.Issue{ + RepoID: r.ID, + PosterID: d.ID, + Poster: d, + Title: title, + Content: content, + IsPull: ispull, + Index: idx, + } + + ctx, committer, err := db.TxContext() + assert.NoError(t, err) + defer committer.Close() + err = issues_model.NewIssueWithIndex(ctx, d, issues_model.NewIssueOptions{ + Repo: r, + Issue: i, + }) + assert.NoError(t, err) + i, err = issues_model.GetIssueByID(ctx, i.ID) + assert.NoError(t, err) + 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) *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 := &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) *issues_model.Comment { + d := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: doer}).(*user_model.User) + 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, committer.Commit()) + return c +} diff --git a/models/issues/label.go b/models/issues/label.go new file mode 100644 index 0000000000..98e2e43961 --- /dev/null +++ b/models/issues/label.go @@ -0,0 +1,836 @@ +// Copyright 2016 The Gogs Authors. All rights reserved. +// Copyright 2020 The Gitea Authors. +// Use of this source code is governed by a MIT-style +// license that can be found in the LICENSE file. + +package issues + +import ( + "context" + "fmt" + "html/template" + "math" + "regexp" + "strconv" + "strings" + + "code.gitea.io/gitea/models/db" + user_model "code.gitea.io/gitea/models/user" + "code.gitea.io/gitea/modules/timeutil" + + "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})$") + +// Label represents a label of repository for issues. +type Label struct { + ID int64 `xorm:"pk autoincr"` + RepoID int64 `xorm:"INDEX"` + OrgID int64 `xorm:"INDEX"` + Name string + Description string + Color string `xorm:"VARCHAR(7)"` + NumIssues int + NumClosedIssues int + CreatedUnix timeutil.TimeStamp `xorm:"INDEX created"` + UpdatedUnix timeutil.TimeStamp `xorm:"INDEX updated"` + + NumOpenIssues int `xorm:"-"` + NumOpenRepoIssues int64 `xorm:"-"` + IsChecked bool `xorm:"-"` + QueryString string `xorm:"-"` + IsSelected bool `xorm:"-"` + IsExcluded bool `xorm:"-"` +} + +func init() { + db.RegisterModel(new(Label)) + db.RegisterModel(new(IssueLabel)) +} + +// CalOpenIssues sets the number of open issues of a label based on the already stored number of closed issues. +func (label *Label) CalOpenIssues() { + label.NumOpenIssues = label.NumIssues - label.NumClosedIssues +} + +// CalOpenOrgIssues calculates the open issues of a label for a specific repo +func (label *Label) CalOpenOrgIssues(repoID, labelID int64) { + counts, _ := CountIssuesByRepo(&IssuesOptions{ + RepoID: repoID, + LabelIDs: []int64{labelID}, + }) + + for _, count := range counts { + label.NumOpenRepoIssues += count + } +} + +// LoadSelectedLabelsAfterClick calculates the set of selected labels when a label is clicked +func (label *Label) LoadSelectedLabelsAfterClick(currentSelectedLabels []int64) { + var labelQuerySlice []string + labelSelected := false + labelID := strconv.FormatInt(label.ID, 10) + for _, s := range currentSelectedLabels { + if s == label.ID { + labelSelected = true + } else if -s == label.ID { + labelSelected = true + label.IsExcluded = true + } else if s != 0 { + labelQuerySlice = append(labelQuerySlice, strconv.FormatInt(s, 10)) + } + } + if !labelSelected { + labelQuerySlice = append(labelQuerySlice, labelID) + } + label.IsSelected = labelSelected + label.QueryString = strings.Join(labelQuerySlice, ",") +} + +// BelongsToOrg returns true if label is an organization label +func (label *Label) BelongsToOrg() bool { + return label.OrgID > 0 +} + +// BelongsToRepo returns true if label is a repository label +func (label *Label) BelongsToRepo() bool { + return label.RepoID > 0 +} + +// SrgbToLinear converts a component of an sRGB color to its linear intensity +// See: https://en.wikipedia.org/wiki/SRGB#The_reverse_transformation_(sRGB_to_CIE_XYZ) +func SrgbToLinear(color uint8) float64 { + flt := float64(color) / 255 + if flt <= 0.04045 { + return flt / 12.92 + } + return math.Pow((flt+0.055)/1.055, 2.4) +} + +// Luminance returns the luminance of an sRGB color +func Luminance(color uint32) float64 { + r := SrgbToLinear(uint8(0xFF & (color >> 16))) + g := SrgbToLinear(uint8(0xFF & (color >> 8))) + b := SrgbToLinear(uint8(0xFF & color)) + + // luminance ratios for sRGB + return 0.2126*r + 0.7152*g + 0.0722*b +} + +// LuminanceThreshold is the luminance at which white and black appear to have the same contrast +// i.e. x such that 1.05 / (x + 0.05) = (x + 0.05) / 0.05 +// i.e. math.Sqrt(1.05*0.05) - 0.05 +const LuminanceThreshold float64 = 0.179 + +// ForegroundColor calculates the text color for labels based +// on their background color. +func (label *Label) ForegroundColor() template.CSS { + if strings.HasPrefix(label.Color, "#") { + if color, err := strconv.ParseUint(label.Color[1:], 16, 64); err == nil { + // NOTE: see web_src/js/components/ContextPopup.vue for similar implementation + luminance := Luminance(uint32(color)) + + // prefer white or black based upon contrast + if luminance < LuminanceThreshold { + return template.CSS("#fff") + } + return template.CSS("#000") + } + } + + // default to black + return template.CSS("#000") +} + +// NewLabel creates a new label +func NewLabel(ctx context.Context, label *Label) error { + if !LabelColorPattern.MatchString(label.Color) { + return fmt.Errorf("bad color code: %s", label.Color) + } + + // normalize case + label.Color = strings.ToLower(label.Color) + + // add leading hash + if label.Color[0] != '#' { + label.Color = "#" + label.Color + } + + // convert 3-character shorthand into 6-character version + if len(label.Color) == 4 { + r := label.Color[1] + g := label.Color[2] + b := label.Color[3] + label.Color = fmt.Sprintf("#%c%c%c%c%c%c", r, r, g, g, b, b) + } + + return db.Insert(ctx, label) +} + +// NewLabels creates new labels +func NewLabels(labels ...*Label) error { + ctx, committer, err := db.TxContext() + if err != nil { + return err + } + defer committer.Close() + + for _, label := range labels { + if !LabelColorPattern.MatchString(label.Color) { + return fmt.Errorf("bad color code: %s", label.Color) + } + if err := db.Insert(ctx, label); err != nil { + return err + } + } + return committer.Commit() +} + +// UpdateLabel updates label information. +func UpdateLabel(l *Label) error { + if !LabelColorPattern.MatchString(l.Color) { + return fmt.Errorf("bad color code: %s", l.Color) + } + return updateLabelCols(db.DefaultContext, l, "name", "description", "color") +} + +// DeleteLabel delete a label +func DeleteLabel(id, labelID int64) error { + label, err := GetLabelByID(db.DefaultContext, labelID) + if err != nil { + if IsErrLabelNotExist(err) { + return nil + } + return err + } + + ctx, committer, err := db.TxContext() + if err != nil { + return err + } + defer committer.Close() + + sess := db.GetEngine(ctx) + + if label.BelongsToOrg() && label.OrgID != id { + return nil + } + if label.BelongsToRepo() && label.RepoID != id { + return nil + } + + if _, err = sess.ID(labelID).Delete(new(Label)); err != nil { + return err + } else if _, err = sess. + Where("label_id = ?", labelID). + Delete(new(IssueLabel)); err != nil { + return err + } + + // delete comments about now deleted label_id + if _, err = sess.Where("label_id = ?", labelID).Cols("label_id").Delete(&Comment{}); err != nil { + return err + } + + return committer.Commit() +} + +// GetLabelByID returns a label by given ID. +func GetLabelByID(ctx context.Context, labelID int64) (*Label, error) { + if labelID <= 0 { + return nil, ErrLabelNotExist{labelID} + } + + l := &Label{} + has, err := db.GetEngine(ctx).ID(labelID).Get(l) + if err != nil { + return nil, err + } else if !has { + return nil, ErrLabelNotExist{l.ID} + } + return l, nil +} + +// GetLabelsByIDs returns a list of labels by IDs +func GetLabelsByIDs(labelIDs []int64) ([]*Label, error) { + labels := make([]*Label, 0, len(labelIDs)) + return labels, db.GetEngine(db.DefaultContext).Table("label"). + In("id", labelIDs). + Asc("name"). + Cols("id", "repo_id", "org_id"). + Find(&labels) +} + +// __________ .__ __ +// \______ \ ____ ______ ____ _____|__|/ |_ ___________ ___.__. +// | _// __ \\____ \ / _ \/ ___/ \ __\/ _ \_ __ < | | +// | | \ ___/| |_> > <_> )___ \| || | ( <_> ) | \/\___ | +// |____|_ /\___ > __/ \____/____ >__||__| \____/|__| / ____| +// \/ \/|__| \/ \/ + +// GetLabelInRepoByName returns a label by name in given repository. +func GetLabelInRepoByName(ctx context.Context, repoID int64, labelName string) (*Label, error) { + if len(labelName) == 0 || repoID <= 0 { + return nil, ErrRepoLabelNotExist{0, repoID} + } + + l := &Label{ + Name: labelName, + RepoID: repoID, + } + has, err := db.GetByBean(ctx, l) + if err != nil { + return nil, err + } else if !has { + return nil, ErrRepoLabelNotExist{0, l.RepoID} + } + return l, nil +} + +// GetLabelInRepoByID returns a label by ID in given repository. +func GetLabelInRepoByID(ctx context.Context, repoID, labelID int64) (*Label, error) { + if labelID <= 0 || repoID <= 0 { + return nil, ErrRepoLabelNotExist{labelID, repoID} + } + + l := &Label{ + ID: labelID, + RepoID: repoID, + } + has, err := db.GetByBean(ctx, l) + if err != nil { + return nil, err + } else if !has { + return nil, ErrRepoLabelNotExist{l.ID, l.RepoID} + } + return l, nil +} + +// GetLabelIDsInRepoByNames returns a list of labelIDs by names in a given +// repository. +// it silently ignores label names that do not belong to the repository. +func GetLabelIDsInRepoByNames(repoID int64, labelNames []string) ([]int64, error) { + labelIDs := make([]int64, 0, len(labelNames)) + return labelIDs, db.GetEngine(db.DefaultContext).Table("label"). + Where("repo_id = ?", repoID). + In("name", labelNames). + Asc("name"). + Cols("id"). + Find(&labelIDs) +} + +// BuildLabelNamesIssueIDsCondition returns a builder where get issue ids match label names +func BuildLabelNamesIssueIDsCondition(labelNames []string) *builder.Builder { + return builder.Select("issue_label.issue_id"). + From("issue_label"). + InnerJoin("label", "label.id = issue_label.label_id"). + Where( + builder.In("label.name", labelNames), + ). + GroupBy("issue_label.issue_id") +} + +// GetLabelsInRepoByIDs returns a list of labels by IDs in given repository, +// it silently ignores label IDs that do not belong to the repository. +func GetLabelsInRepoByIDs(repoID int64, labelIDs []int64) ([]*Label, error) { + labels := make([]*Label, 0, len(labelIDs)) + return labels, db.GetEngine(db.DefaultContext). + Where("repo_id = ?", repoID). + In("id", labelIDs). + Asc("name"). + Find(&labels) +} + +// GetLabelsByRepoID returns all labels that belong to given repository by ID. +func GetLabelsByRepoID(ctx context.Context, repoID int64, sortType string, listOptions db.ListOptions) ([]*Label, error) { + if repoID <= 0 { + return nil, ErrRepoLabelNotExist{0, repoID} + } + labels := make([]*Label, 0, 10) + sess := db.GetEngine(ctx).Where("repo_id = ?", repoID) + + switch sortType { + case "reversealphabetically": + sess.Desc("name") + case "leastissues": + sess.Asc("num_issues") + case "mostissues": + sess.Desc("num_issues") + default: + sess.Asc("name") + } + + if listOptions.Page != 0 { + sess = db.SetSessionPagination(sess, &listOptions) + } + + return labels, sess.Find(&labels) +} + +// CountLabelsByRepoID count number of all labels that belong to given repository by ID. +func CountLabelsByRepoID(repoID int64) (int64, error) { + return db.GetEngine(db.DefaultContext).Where("repo_id = ?", repoID).Count(&Label{}) +} + +// ________ +// \_____ \_______ ____ +// / | \_ __ \/ ___\ +// / | \ | \/ /_/ > +// \_______ /__| \___ / +// \/ /_____/ + +// GetLabelInOrgByName returns a label by name in given organization. +func GetLabelInOrgByName(ctx context.Context, orgID int64, labelName string) (*Label, error) { + if len(labelName) == 0 || orgID <= 0 { + return nil, ErrOrgLabelNotExist{0, orgID} + } + + l := &Label{ + Name: labelName, + OrgID: orgID, + } + has, err := db.GetByBean(ctx, l) + if err != nil { + return nil, err + } else if !has { + return nil, ErrOrgLabelNotExist{0, l.OrgID} + } + return l, nil +} + +// GetLabelInOrgByID returns a label by ID in given organization. +func GetLabelInOrgByID(ctx context.Context, orgID, labelID int64) (*Label, error) { + if labelID <= 0 || orgID <= 0 { + return nil, ErrOrgLabelNotExist{labelID, orgID} + } + + l := &Label{ + ID: labelID, + OrgID: orgID, + } + has, err := db.GetByBean(ctx, l) + if err != nil { + return nil, err + } else if !has { + return nil, ErrOrgLabelNotExist{l.ID, l.OrgID} + } + return l, nil +} + +// GetLabelIDsInOrgByNames returns a list of labelIDs by names in a given +// organization. +func GetLabelIDsInOrgByNames(orgID int64, labelNames []string) ([]int64, error) { + if orgID <= 0 { + return nil, ErrOrgLabelNotExist{0, orgID} + } + labelIDs := make([]int64, 0, len(labelNames)) + + return labelIDs, db.GetEngine(db.DefaultContext).Table("label"). + Where("org_id = ?", orgID). + In("name", labelNames). + Asc("name"). + Cols("id"). + Find(&labelIDs) +} + +// GetLabelsInOrgByIDs returns a list of labels by IDs in given organization, +// it silently ignores label IDs that do not belong to the organization. +func GetLabelsInOrgByIDs(orgID int64, labelIDs []int64) ([]*Label, error) { + labels := make([]*Label, 0, len(labelIDs)) + return labels, db.GetEngine(db.DefaultContext). + Where("org_id = ?", orgID). + In("id", labelIDs). + Asc("name"). + Find(&labels) +} + +// GetLabelsByOrgID returns all labels that belong to given organization by ID. +func GetLabelsByOrgID(ctx context.Context, orgID int64, sortType string, listOptions db.ListOptions) ([]*Label, error) { + if orgID <= 0 { + return nil, ErrOrgLabelNotExist{0, orgID} + } + labels := make([]*Label, 0, 10) + sess := db.GetEngine(ctx).Where("org_id = ?", orgID) + + switch sortType { + case "reversealphabetically": + sess.Desc("name") + case "leastissues": + sess.Asc("num_issues") + case "mostissues": + sess.Desc("num_issues") + default: + sess.Asc("name") + } + + if listOptions.Page != 0 { + sess = db.SetSessionPagination(sess, &listOptions) + } + + return labels, sess.Find(&labels) +} + +// CountLabelsByOrgID count all labels that belong to given organization by ID. +func CountLabelsByOrgID(orgID int64) (int64, error) { + return db.GetEngine(db.DefaultContext).Where("org_id = ?", orgID).Count(&Label{}) +} + +// .___ +// | | ______ ________ __ ____ +// | |/ ___// ___/ | \_/ __ \ +// | |\___ \ \___ \| | /\ ___/ +// |___/____ >____ >____/ \___ | +// \/ \/ \/ + +// GetLabelsByIssueID returns all labels that belong to given issue by ID. +func GetLabelsByIssueID(ctx context.Context, issueID int64) ([]*Label, error) { + var labels []*Label + return labels, db.GetEngine(ctx).Where("issue_label.issue_id = ?", issueID). + Join("LEFT", "issue_label", "issue_label.label_id = label.id"). + Asc("label.name"). + Find(&labels) +} + +func updateLabelCols(ctx context.Context, l *Label, cols ...string) error { + _, err := db.GetEngine(ctx).ID(l.ID). + SetExpr("num_issues", + builder.Select("count(*)").From("issue_label"). + Where(builder.Eq{"label_id": l.ID}), + ). + SetExpr("num_closed_issues", + builder.Select("count(*)").From("issue_label"). + InnerJoin("issue", "issue_label.issue_id = issue.id"). + Where(builder.Eq{ + "issue_label.label_id": l.ID, + "issue.is_closed": true, + }), + ). + Cols(cols...).Update(l) + return err +} + +// .___ .____ ___. .__ +// | | ______ ________ __ ____ | | _____ \_ |__ ____ | | +// | |/ ___// ___/ | \_/ __ \| | \__ \ | __ \_/ __ \| | +// | |\___ \ \___ \| | /\ ___/| |___ / __ \| \_\ \ ___/| |__ +// |___/____ >____ >____/ \___ >_______ (____ /___ /\___ >____/ +// \/ \/ \/ \/ \/ \/ \/ + +// IssueLabel represents an issue-label relation. +type IssueLabel struct { + ID int64 `xorm:"pk autoincr"` + IssueID int64 `xorm:"UNIQUE(s)"` + LabelID int64 `xorm:"UNIQUE(s)"` +} + +// HasIssueLabel returns true if issue has been labeled. +func HasIssueLabel(ctx context.Context, issueID, labelID int64) bool { + has, _ := db.GetEngine(ctx).Where("issue_id = ? AND label_id = ?", issueID, labelID).Get(new(IssueLabel)) + return has +} + +// newIssueLabel this function creates a new label it does not check if the label is valid for the issue +// YOU MUST CHECK THIS BEFORE THIS FUNCTION +func newIssueLabel(ctx context.Context, issue *Issue, label *Label, doer *user_model.User) (err error) { + if err = db.Insert(ctx, &IssueLabel{ + IssueID: issue.ID, + LabelID: label.ID, + }); err != nil { + return err + } + + if err = issue.LoadRepo(ctx); err != nil { + return + } + + opts := &CreateCommentOptions{ + Type: CommentTypeLabel, + Doer: doer, + Repo: issue.Repo, + Issue: issue, + Label: label, + Content: "1", + } + if _, err = CreateCommentCtx(ctx, opts); err != nil { + return err + } + + return updateLabelCols(ctx, label, "num_issues", "num_closed_issue") +} + +// NewIssueLabel creates a new issue-label relation. +func NewIssueLabel(issue *Issue, label *Label, doer *user_model.User) (err error) { + if HasIssueLabel(db.DefaultContext, issue.ID, label.ID) { + return nil + } + + ctx, committer, err := db.TxContext() + if err != nil { + return err + } + defer committer.Close() + + if err = issue.LoadRepo(ctx); err != nil { + return err + } + + // Do NOT add invalid labels + if issue.RepoID != label.RepoID && issue.Repo.OwnerID != label.OrgID { + return nil + } + + if err = newIssueLabel(ctx, issue, label, doer); err != nil { + return err + } + + issue.Labels = nil + if err = issue.LoadLabels(ctx); err != nil { + return err + } + + return committer.Commit() +} + +// newIssueLabels add labels to an issue. It will check if the labels are valid for the issue +func newIssueLabels(ctx context.Context, issue *Issue, labels []*Label, doer *user_model.User) (err error) { + if err = issue.LoadRepo(ctx); err != nil { + return err + } + for _, label := range labels { + // Don't add already present labels and invalid labels + if HasIssueLabel(ctx, issue.ID, label.ID) || + (label.RepoID != issue.RepoID && label.OrgID != issue.Repo.OwnerID) { + continue + } + + if err = newIssueLabel(ctx, issue, label, doer); err != nil { + return fmt.Errorf("newIssueLabel: %v", err) + } + } + + return nil +} + +// NewIssueLabels creates a list of issue-label relations. +func NewIssueLabels(issue *Issue, labels []*Label, doer *user_model.User) (err error) { + ctx, committer, err := db.TxContext() + if err != nil { + return err + } + defer committer.Close() + + if err = newIssueLabels(ctx, issue, labels, doer); err != nil { + return err + } + + issue.Labels = nil + if err = issue.LoadLabels(ctx); err != nil { + return err + } + + return committer.Commit() +} + +func deleteIssueLabel(ctx context.Context, issue *Issue, label *Label, doer *user_model.User) (err error) { + if count, err := db.DeleteByBean(ctx, &IssueLabel{ + IssueID: issue.ID, + LabelID: label.ID, + }); err != nil { + return err + } else if count == 0 { + return nil + } + + if err = issue.LoadRepo(ctx); err != nil { + return + } + + opts := &CreateCommentOptions{ + Type: CommentTypeLabel, + Doer: doer, + Repo: issue.Repo, + Issue: issue, + Label: label, + } + if _, err = CreateCommentCtx(ctx, opts); err != nil { + return err + } + + return updateLabelCols(ctx, label, "num_issues", "num_closed_issue") +} + +// DeleteIssueLabel deletes issue-label relation. +func DeleteIssueLabel(ctx context.Context, issue *Issue, label *Label, doer *user_model.User) error { + if err := deleteIssueLabel(ctx, issue, label, doer); err != nil { + return err + } + + issue.Labels = nil + return issue.LoadLabels(ctx) +} + +// 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). + Delete(&IssueLabel{}); err != nil { + return err + } + + _, 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/issues/pull.go b/models/issues/pull.go new file mode 100644 index 0000000000..f2ca19b03e --- /dev/null +++ b/models/issues/pull.go @@ -0,0 +1,838 @@ +// Copyright 2015 The Gogs Authors. All rights reserved. +// Copyright 2019 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 ( + "context" + "fmt" + "io" + "strings" + + "code.gitea.io/gitea/models/db" + git_model "code.gitea.io/gitea/models/git" + pull_model "code.gitea.io/gitea/models/pull" + repo_model "code.gitea.io/gitea/models/repo" + user_model "code.gitea.io/gitea/models/user" + "code.gitea.io/gitea/modules/git" + "code.gitea.io/gitea/modules/log" + "code.gitea.io/gitea/modules/setting" + "code.gitea.io/gitea/modules/timeutil" + "code.gitea.io/gitea/modules/util" + + "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 + +// Enumerate all the pull request types +const ( + PullRequestGitea PullRequestType = iota + PullRequestGit +) + +// PullRequestStatus defines pull request status +type PullRequestStatus int + +// Enumerate all the pull request status +const ( + PullRequestStatusConflict PullRequestStatus = iota + PullRequestStatusChecking + PullRequestStatusMergeable + PullRequestStatusManuallyMerged + PullRequestStatusError + PullRequestStatusEmpty +) + +// PullRequestFlow the flow of pull request +type PullRequestFlow int + +const ( + // PullRequestFlowGithub github flow from head branch to base branch + PullRequestFlowGithub PullRequestFlow = iota + // PullRequestFlowAGit Agit flow pull request, head branch is not exist + PullRequestFlowAGit +) + +// PullRequest represents relation between pull request and repositories. +type PullRequest struct { + ID int64 `xorm:"pk autoincr"` + Type PullRequestType + Status PullRequestStatus + ConflictedFiles []string `xorm:"TEXT JSON"` + CommitsAhead int + CommitsBehind int + + ChangedProtectedFiles []string `xorm:"TEXT JSON"` + + IssueID int64 `xorm:"INDEX"` + Issue *Issue `xorm:"-"` + Index int64 + + HeadRepoID int64 `xorm:"INDEX"` + HeadRepo *repo_model.Repository `xorm:"-"` + BaseRepoID int64 `xorm:"INDEX"` + BaseRepo *repo_model.Repository `xorm:"-"` + HeadBranch string + HeadCommitID string `xorm:"-"` + BaseBranch string + ProtectedBranch *git_model.ProtectedBranch `xorm:"-"` + MergeBase string `xorm:"VARCHAR(40)"` + AllowMaintainerEdit bool `xorm:"NOT NULL DEFAULT false"` + + HasMerged bool `xorm:"INDEX"` + MergedCommitID string `xorm:"VARCHAR(40)"` + MergerID int64 `xorm:"INDEX"` + Merger *user_model.User `xorm:"-"` + MergedUnix timeutil.TimeStamp `xorm:"updated INDEX"` + + isHeadRepoLoaded bool `xorm:"-"` + + Flow PullRequestFlow `xorm:"NOT NULL DEFAULT 0"` +} + +func init() { + db.RegisterModel(new(PullRequest)) +} + +// 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 + if _, err := db.GetEngine(ctx).In("pull_id", deleteCond). + Delete(&pull_model.AutoMerge{}); err != nil { + return err + } + + // Delete review states + if _, err := db.GetEngine(ctx).In("pull_id", deleteCond). + Delete(&pull_model.ReviewState{}); err != nil { + return err + } + + _, err := db.DeleteByBean(ctx, &PullRequest{BaseRepoID: repoID}) + return err +} + +// MustHeadUserName returns the HeadRepo's username if failed return blank +func (pr *PullRequest) MustHeadUserName() string { + if err := pr.LoadHeadRepo(); err != nil { + if !repo_model.IsErrRepoNotExist(err) { + log.Error("LoadHeadRepo: %v", err) + } else { + log.Warn("LoadHeadRepo %d but repository does not exist: %v", pr.HeadRepoID, err) + } + return "" + } + if pr.HeadRepo == nil { + return "" + } + return pr.HeadRepo.OwnerName +} + +// Note: don't try to get Issue because will end up recursive querying. +func (pr *PullRequest) loadAttributes(ctx context.Context) (err error) { + if pr.HasMerged && pr.Merger == nil { + pr.Merger, err = user_model.GetUserByIDCtx(ctx, pr.MergerID) + if user_model.IsErrUserNotExist(err) { + pr.MergerID = -1 + pr.Merger = user_model.NewGhostUser() + } else if err != nil { + return fmt.Errorf("getUserByID [%d]: %v", pr.MergerID, err) + } + } + + return nil +} + +// LoadAttributes loads pull request attributes from database +func (pr *PullRequest) LoadAttributes() error { + return pr.loadAttributes(db.DefaultContext) +} + +// LoadHeadRepoCtx loads the head repository +func (pr *PullRequest) LoadHeadRepoCtx(ctx context.Context) (err error) { + if !pr.isHeadRepoLoaded && pr.HeadRepo == nil && pr.HeadRepoID > 0 { + if pr.HeadRepoID == pr.BaseRepoID { + if pr.BaseRepo != nil { + pr.HeadRepo = pr.BaseRepo + return nil + } else if pr.Issue != nil && pr.Issue.Repo != nil { + pr.HeadRepo = pr.Issue.Repo + return nil + } + } + + pr.HeadRepo, err = repo_model.GetRepositoryByIDCtx(ctx, pr.HeadRepoID) + if err != nil && !repo_model.IsErrRepoNotExist(err) { // Head repo maybe deleted, but it should still work + return fmt.Errorf("getRepositoryByID(head): %v", err) + } + pr.isHeadRepoLoaded = true + } + return nil +} + +// LoadHeadRepo loads the head repository +func (pr *PullRequest) LoadHeadRepo() error { + return pr.LoadHeadRepoCtx(db.DefaultContext) +} + +// LoadBaseRepo loads the target repository +func (pr *PullRequest) LoadBaseRepo() error { + return pr.LoadBaseRepoCtx(db.DefaultContext) +} + +// LoadBaseRepoCtx loads the target repository +func (pr *PullRequest) LoadBaseRepoCtx(ctx context.Context) (err error) { + if pr.BaseRepo != nil { + return nil + } + + if pr.HeadRepoID == pr.BaseRepoID && pr.HeadRepo != nil { + pr.BaseRepo = pr.HeadRepo + return nil + } + + if pr.Issue != nil && pr.Issue.Repo != nil { + pr.BaseRepo = pr.Issue.Repo + return nil + } + + pr.BaseRepo, err = repo_model.GetRepositoryByIDCtx(ctx, pr.BaseRepoID) + if err != nil { + return fmt.Errorf("repo_model.GetRepositoryByID(base): %v", err) + } + return nil +} + +// LoadIssue loads issue information from database +func (pr *PullRequest) LoadIssue() (err error) { + return pr.LoadIssueCtx(db.DefaultContext) +} + +// LoadIssueCtx loads issue information from database +func (pr *PullRequest) LoadIssueCtx(ctx context.Context) (err error) { + if pr.Issue != nil { + return nil + } + + pr.Issue, err = GetIssueByID(ctx, pr.IssueID) + if err == nil { + pr.Issue.PullRequest = pr + } + return err +} + +// LoadProtectedBranch loads the protected branch of the base branch +func (pr *PullRequest) LoadProtectedBranch() (err error) { + return pr.LoadProtectedBranchCtx(db.DefaultContext) +} + +// LoadProtectedBranchCtx loads the protected branch of the base branch +func (pr *PullRequest) LoadProtectedBranchCtx(ctx context.Context) (err error) { + if pr.ProtectedBranch == nil { + if pr.BaseRepo == nil { + if pr.BaseRepoID == 0 { + return nil + } + pr.BaseRepo, err = repo_model.GetRepositoryByIDCtx(ctx, pr.BaseRepoID) + if err != nil { + return + } + } + pr.ProtectedBranch, err = git_model.GetProtectedBranchBy(ctx, pr.BaseRepo.ID, pr.BaseBranch) + } + return +} + +// ReviewCount represents a count of Reviews +type ReviewCount struct { + IssueID int64 + Type ReviewType + Count int64 +} + +// GetApprovalCounts returns the approval counts by type +// FIXME: Only returns official counts due to double counting of non-official counts +func (pr *PullRequest) GetApprovalCounts(ctx context.Context) ([]*ReviewCount, error) { + rCounts := make([]*ReviewCount, 0, 6) + sess := db.GetEngine(ctx).Where("issue_id = ?", pr.IssueID) + return rCounts, sess.Select("issue_id, type, count(id) as `count`").Where("official = ? AND dismissed = ?", true, false).GroupBy("issue_id, type").Table("review").Find(&rCounts) +} + +// GetApprovers returns the approvers of the pull request +func (pr *PullRequest) GetApprovers() string { + stringBuilder := strings.Builder{} + if err := pr.getReviewedByLines(&stringBuilder); err != nil { + log.Error("Unable to getReviewedByLines: Error: %v", err) + return "" + } + + return stringBuilder.String() +} + +func (pr *PullRequest) getReviewedByLines(writer io.Writer) error { + maxReviewers := setting.Repository.PullRequest.DefaultMergeMessageMaxApprovers + + if maxReviewers == 0 { + return nil + } + + ctx, committer, err := db.TxContext() + if err != nil { + return err + } + defer committer.Close() + + // Note: This doesn't page as we only expect a very limited number of reviews + reviews, err := FindReviews(ctx, FindReviewOptions{ + Type: ReviewTypeApprove, + IssueID: pr.IssueID, + OfficialOnly: setting.Repository.PullRequest.DefaultMergeMessageOfficialApproversOnly, + }) + if err != nil { + log.Error("Unable to FindReviews for PR ID %d: %v", pr.ID, err) + return err + } + + reviewersWritten := 0 + + for _, review := range reviews { + if maxReviewers > 0 && reviewersWritten > maxReviewers { + break + } + + if err := review.loadReviewer(ctx); err != nil && !user_model.IsErrUserNotExist(err) { + log.Error("Unable to LoadReviewer[%d] for PR ID %d : %v", review.ReviewerID, pr.ID, err) + return err + } else if review.Reviewer == nil { + continue + } + if _, err := writer.Write([]byte("Reviewed-by: ")); err != nil { + return err + } + if _, err := writer.Write([]byte(review.Reviewer.NewGitSig().String())); err != nil { + return err + } + if _, err := writer.Write([]byte{'\n'}); err != nil { + return err + } + reviewersWritten++ + } + return committer.Commit() +} + +// GetGitRefName returns git ref for hidden pull request branch +func (pr *PullRequest) GetGitRefName() string { + return fmt.Sprintf("%s%d/head", git.PullPrefix, pr.Index) +} + +// IsChecking returns true if this pull request is still checking conflict. +func (pr *PullRequest) IsChecking() bool { + return pr.Status == PullRequestStatusChecking +} + +// CanAutoMerge returns true if this pull request can be merged automatically. +func (pr *PullRequest) CanAutoMerge() bool { + return pr.Status == PullRequestStatusMergeable +} + +// IsEmpty returns true if this pull request is empty. +func (pr *PullRequest) IsEmpty() bool { + return pr.Status == PullRequestStatusEmpty +} + +// SetMerged sets a pull request to merged and closes the corresponding issue +func (pr *PullRequest) SetMerged(ctx context.Context) (bool, error) { + if pr.HasMerged { + return false, fmt.Errorf("PullRequest[%d] already merged", pr.Index) + } + if pr.MergedCommitID == "" || pr.MergedUnix == 0 || pr.Merger == nil { + return false, fmt.Errorf("Unable to merge PullRequest[%d], some required fields are empty", pr.Index) + } + + pr.HasMerged = true + sess := db.GetEngine(ctx) + + if _, err := sess.Exec("UPDATE `issue` SET `repo_id` = `repo_id` WHERE `id` = ?", pr.IssueID); err != nil { + return false, err + } + + if _, err := sess.Exec("UPDATE `pull_request` SET `issue_id` = `issue_id` WHERE `id` = ?", pr.ID); err != nil { + return false, err + } + + pr.Issue = nil + if err := pr.LoadIssueCtx(ctx); err != nil { + return false, err + } + + if tmpPr, err := GetPullRequestByID(ctx, pr.ID); err != nil { + return false, err + } else if tmpPr.HasMerged { + if pr.Issue.IsClosed { + return false, nil + } + return false, fmt.Errorf("PullRequest[%d] already merged but it's associated issue [%d] is not closed", pr.Index, pr.IssueID) + } else if pr.Issue.IsClosed { + return false, fmt.Errorf("PullRequest[%d] already closed", pr.Index) + } + + if err := pr.Issue.LoadRepo(ctx); err != nil { + return false, err + } + + if err := pr.Issue.Repo.GetOwner(ctx); err != nil { + return false, err + } + + if _, err := changeIssueStatus(ctx, pr.Issue, pr.Merger, true, true); err != nil { + return false, fmt.Errorf("Issue.changeStatus: %v", err) + } + + // reset the conflicted files as there cannot be any if we're merged + pr.ConflictedFiles = []string{} + + // We need to save all of the data used to compute this merge as it may have already been changed by TestPatch. FIXME: need to set some state to prevent TestPatch from running whilst we are merging. + if _, err := sess.Where("id = ?", pr.ID).Cols("has_merged, status, merge_base, merged_commit_id, merger_id, merged_unix, conflicted_files").Update(pr); err != nil { + return false, fmt.Errorf("Failed to update pr[%d]: %v", pr.ID, err) + } + + return true, nil +} + +// NewPullRequest creates new pull request with labels for repository. +func NewPullRequest(outerCtx context.Context, repo *repo_model.Repository, issue *Issue, labelIDs []int64, uuids []string, pr *PullRequest) (err error) { + idx, err := db.GetNextResourceIndex("issue_index", repo.ID) + if err != nil { + return fmt.Errorf("generate pull request index failed: %v", err) + } + + issue.Index = idx + + ctx, committer, err := db.TxContext() + if err != nil { + return err + } + defer committer.Close() + ctx.WithContext(outerCtx) + + if err = NewIssueWithIndex(ctx, issue.Poster, NewIssueOptions{ + Repo: repo, + Issue: issue, + LabelIDs: labelIDs, + Attachments: uuids, + IsPull: true, + }); err != nil { + if repo_model.IsErrUserDoesNotHaveAccessToRepo(err) || IsErrNewIssueInsert(err) { + return err + } + return fmt.Errorf("newIssue: %v", err) + } + + pr.Index = issue.Index + pr.BaseRepo = repo + pr.IssueID = issue.ID + if err = db.Insert(ctx, pr); err != nil { + return fmt.Errorf("insert pull repo: %v", err) + } + + if err = committer.Commit(); err != nil { + return fmt.Errorf("Commit: %v", err) + } + + return nil +} + +// GetUnmergedPullRequest returns a pull request that is open and has not been merged +// by given head/base and repo/branch. +func GetUnmergedPullRequest(headRepoID, baseRepoID int64, headBranch, baseBranch string, flow PullRequestFlow) (*PullRequest, error) { + pr := new(PullRequest) + has, err := db.GetEngine(db.DefaultContext). + Where("head_repo_id=? AND head_branch=? AND base_repo_id=? AND base_branch=? AND has_merged=? AND flow = ? AND issue.is_closed=?", + headRepoID, headBranch, baseRepoID, baseBranch, false, flow, false). + Join("INNER", "issue", "issue.id=pull_request.issue_id"). + Get(pr) + if err != nil { + return nil, err + } else if !has { + return nil, ErrPullRequestNotExist{0, 0, headRepoID, baseRepoID, headBranch, baseBranch} + } + + return pr, nil +} + +// GetLatestPullRequestByHeadInfo returns the latest pull request (regardless of its status) +// by given head information (repo and branch). +func GetLatestPullRequestByHeadInfo(repoID int64, branch string) (*PullRequest, error) { + pr := new(PullRequest) + has, err := db.GetEngine(db.DefaultContext). + Where("head_repo_id = ? AND head_branch = ? AND flow = ?", repoID, branch, PullRequestFlowGithub). + OrderBy("id DESC"). + Get(pr) + if !has { + return nil, err + } + return pr, err +} + +// GetPullRequestByIndex returns a pull request by the given index +func GetPullRequestByIndex(ctx context.Context, repoID, index int64) (*PullRequest, error) { + if index < 1 { + return nil, ErrPullRequestNotExist{} + } + pr := &PullRequest{ + BaseRepoID: repoID, + Index: index, + } + + has, err := db.GetEngine(ctx).Get(pr) + if err != nil { + return nil, err + } else if !has { + return nil, ErrPullRequestNotExist{0, 0, 0, repoID, "", ""} + } + + if err = pr.loadAttributes(ctx); err != nil { + return nil, err + } + if err = pr.LoadIssueCtx(ctx); err != nil { + return nil, err + } + + return pr, nil +} + +// GetPullRequestByID returns a pull request by given ID. +func GetPullRequestByID(ctx context.Context, id int64) (*PullRequest, error) { + pr := new(PullRequest) + has, err := db.GetEngine(ctx).ID(id).Get(pr) + if err != nil { + return nil, err + } else if !has { + return nil, ErrPullRequestNotExist{id, 0, 0, 0, "", ""} + } + return pr, pr.loadAttributes(ctx) +} + +// GetPullRequestByIssueIDWithNoAttributes returns pull request with no attributes loaded by given issue ID. +func GetPullRequestByIssueIDWithNoAttributes(issueID int64) (*PullRequest, error) { + var pr PullRequest + has, err := db.GetEngine(db.DefaultContext).Where("issue_id = ?", issueID).Get(&pr) + if err != nil { + return nil, err + } + if !has { + return nil, ErrPullRequestNotExist{0, issueID, 0, 0, "", ""} + } + return &pr, nil +} + +// GetPullRequestByIssueID returns pull request by given issue ID. +func GetPullRequestByIssueID(ctx context.Context, issueID int64) (*PullRequest, error) { + pr := &PullRequest{ + IssueID: issueID, + } + has, err := db.GetByBean(ctx, pr) + if err != nil { + return nil, err + } else if !has { + return nil, ErrPullRequestNotExist{0, issueID, 0, 0, "", ""} + } + return pr, pr.loadAttributes(ctx) +} + +// GetAllUnmergedAgitPullRequestByPoster get all unmerged agit flow pull request +// By poster id. +func GetAllUnmergedAgitPullRequestByPoster(uid int64) ([]*PullRequest, error) { + pulls := make([]*PullRequest, 0, 10) + + err := db.GetEngine(db.DefaultContext). + Where("has_merged=? AND flow = ? AND issue.is_closed=? AND issue.poster_id=?", + false, PullRequestFlowAGit, false, uid). + Join("INNER", "issue", "issue.id=pull_request.issue_id"). + Find(&pulls) + + return pulls, err +} + +// Update updates all fields of pull request. +func (pr *PullRequest) Update() error { + _, err := db.GetEngine(db.DefaultContext).ID(pr.ID).AllCols().Update(pr) + return err +} + +// UpdateCols updates specific fields of pull request. +func (pr *PullRequest) UpdateCols(cols ...string) error { + _, err := db.GetEngine(db.DefaultContext).ID(pr.ID).Cols(cols...).Update(pr) + return err +} + +// UpdateColsIfNotMerged updates specific fields of a pull request if it has not been merged +func (pr *PullRequest) UpdateColsIfNotMerged(cols ...string) error { + _, err := db.GetEngine(db.DefaultContext).Where("id = ? AND has_merged = ?", pr.ID, false).Cols(cols...).Update(pr) + return err +} + +// IsWorkInProgress determine if the Pull Request is a Work In Progress by its title +func (pr *PullRequest) IsWorkInProgress() bool { + if err := pr.LoadIssue(); err != nil { + log.Error("LoadIssue: %v", err) + return false + } + return HasWorkInProgressPrefix(pr.Issue.Title) +} + +// HasWorkInProgressPrefix determines if the given PR title has a Work In Progress prefix +func HasWorkInProgressPrefix(title string) bool { + for _, prefix := range setting.Repository.PullRequest.WorkInProgressPrefixes { + if strings.HasPrefix(strings.ToUpper(title), strings.ToUpper(prefix)) { + return true + } + } + return false +} + +// IsFilesConflicted determines if the Pull Request has changes conflicting with the target branch. +func (pr *PullRequest) IsFilesConflicted() bool { + return len(pr.ConflictedFiles) > 0 +} + +// GetWorkInProgressPrefix returns the prefix used to mark the pull request as a work in progress. +// It returns an empty string when none were found +func (pr *PullRequest) GetWorkInProgressPrefix() string { + if err := pr.LoadIssue(); err != nil { + log.Error("LoadIssue: %v", err) + return "" + } + + for _, prefix := range setting.Repository.PullRequest.WorkInProgressPrefixes { + if strings.HasPrefix(strings.ToUpper(pr.Issue.Title), strings.ToUpper(prefix)) { + return pr.Issue.Title[0:len(prefix)] + } + } + return "" +} + +// UpdateCommitDivergence update Divergence of a pull request +func (pr *PullRequest) UpdateCommitDivergence(ctx context.Context, ahead, behind int) error { + if pr.ID == 0 { + return fmt.Errorf("pull ID is 0") + } + pr.CommitsAhead = ahead + pr.CommitsBehind = behind + _, err := db.GetEngine(ctx).ID(pr.ID).Cols("commits_ahead", "commits_behind").Update(pr) + return err +} + +// IsSameRepo returns true if base repo and head repo is the same +func (pr *PullRequest) IsSameRepo() bool { + return pr.BaseRepoID == pr.HeadRepoID +} + +// GetPullRequestsByHeadBranch returns all prs by head branch +// Since there could be multiple prs with the same head branch, this function returns a slice of prs +func GetPullRequestsByHeadBranch(ctx context.Context, headBranch string, headRepoID int64) ([]*PullRequest, error) { + log.Trace("GetPullRequestsByHeadBranch: headBranch: '%s', headRepoID: '%d'", headBranch, headRepoID) + prs := make([]*PullRequest, 0, 2) + if err := db.GetEngine(ctx).Where(builder.Eq{"head_branch": headBranch, "head_repo_id": headRepoID}). + Find(&prs); err != nil { + return nil, err + } + return prs, nil +} + +// GetBaseBranchHTMLURL returns the HTML URL of the base branch +func (pr *PullRequest) GetBaseBranchHTMLURL() string { + if err := pr.LoadBaseRepo(); err != nil { + log.Error("LoadBaseRepo: %v", err) + return "" + } + if pr.BaseRepo == nil { + return "" + } + return pr.BaseRepo.HTMLURL() + "/src/branch/" + util.PathEscapeSegments(pr.BaseBranch) +} + +// GetHeadBranchHTMLURL returns the HTML URL of the head branch +func (pr *PullRequest) GetHeadBranchHTMLURL() string { + if pr.Flow == PullRequestFlowAGit { + return "" + } + + if err := pr.LoadHeadRepo(); err != nil { + log.Error("LoadHeadRepo: %v", err) + return "" + } + if pr.HeadRepo == nil { + return "" + } + return pr.HeadRepo.HTMLURL() + "/src/branch/" + util.PathEscapeSegments(pr.HeadBranch) +} + +// UpdateAllowEdits update if PR can be edited from maintainers +func UpdateAllowEdits(ctx context.Context, pr *PullRequest) error { + if _, err := db.GetEngine(ctx).ID(pr.ID).Cols("allow_maintainer_edit").Update(pr); err != nil { + return err + } + return nil +} + +// Mergeable returns if the pullrequest is mergeable. +func (pr *PullRequest) Mergeable() bool { + // If a pull request isn't mergable if it's: + // - Being conflict checked. + // - Has a conflict. + // - Received a error while being conflict checked. + // - Is a work-in-progress pull request. + 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/issues/pull_list.go b/models/issues/pull_list.go new file mode 100644 index 0000000000..9ca536909e --- /dev/null +++ b/models/issues/pull_list.go @@ -0,0 +1,216 @@ +// Copyright 2019 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 ( + "context" + "fmt" + + "code.gitea.io/gitea/models/db" + access_model "code.gitea.io/gitea/models/perm/access" + "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/git" + "code.gitea.io/gitea/modules/log" + + "xorm.io/xorm" +) + +// PullRequestsOptions holds the options for PRs +type PullRequestsOptions struct { + db.ListOptions + State string + SortType string + Labels []string + MilestoneID int64 +} + +func listPullRequestStatement(baseRepoID int64, opts *PullRequestsOptions) (*xorm.Session, error) { + sess := db.GetEngine(db.DefaultContext).Where("pull_request.base_repo_id=?", baseRepoID) + + sess.Join("INNER", "issue", "pull_request.issue_id = issue.id") + switch opts.State { + case "closed", "open": + sess.And("issue.is_closed=?", opts.State == "closed") + } + + if labelIDs, err := base.StringsToInt64s(opts.Labels); err != nil { + return nil, err + } else if len(labelIDs) > 0 { + sess.Join("INNER", "issue_label", "issue.id = issue_label.issue_id"). + In("issue_label.label_id", labelIDs) + } + + if opts.MilestoneID > 0 { + sess.And("issue.milestone_id=?", opts.MilestoneID) + } + + return sess, nil +} + +// GetUnmergedPullRequestsByHeadInfo returns all pull requests that are open and has not been merged +// by given head information (repo and branch). +func GetUnmergedPullRequestsByHeadInfo(repoID int64, branch string) ([]*PullRequest, error) { + prs := make([]*PullRequest, 0, 2) + return prs, db.GetEngine(db.DefaultContext). + Where("head_repo_id = ? AND head_branch = ? AND has_merged = ? AND issue.is_closed = ? AND flow = ?", + repoID, branch, false, false, PullRequestFlowGithub). + Join("INNER", "issue", "issue.id = pull_request.issue_id"). + Find(&prs) +} + +// CanMaintainerWriteToBranch check whether user is a matainer and could write to the branch +func CanMaintainerWriteToBranch(p access_model.Permission, branch string, user *user_model.User) bool { + if p.CanWrite(unit.TypeCode) { + return true + } + + if len(p.Units) < 1 { + return false + } + + prs, err := GetUnmergedPullRequestsByHeadInfo(p.Units[0].RepoID, branch) + if err != nil { + return false + } + + for _, pr := range prs { + if pr.AllowMaintainerEdit { + err = pr.LoadBaseRepo() + if err != nil { + continue + } + prPerm, err := access_model.GetUserRepoPermission(db.DefaultContext, pr.BaseRepo, user) + if err != nil { + continue + } + if prPerm.CanWrite(unit.TypeCode) { + return true + } + } + } + return false +} + +// HasUnmergedPullRequestsByHeadInfo checks if there are open and not merged pull request +// by given head information (repo and branch) +func HasUnmergedPullRequestsByHeadInfo(ctx context.Context, repoID int64, branch string) (bool, error) { + return db.GetEngine(ctx). + Where("head_repo_id = ? AND head_branch = ? AND has_merged = ? AND issue.is_closed = ? AND flow = ?", + repoID, branch, false, false, PullRequestFlowGithub). + Join("INNER", "issue", "issue.id = pull_request.issue_id"). + Exist(&PullRequest{}) +} + +// GetUnmergedPullRequestsByBaseInfo returns all pull requests that are open and has not been merged +// by given base information (repo and branch). +func GetUnmergedPullRequestsByBaseInfo(repoID int64, branch string) ([]*PullRequest, error) { + prs := make([]*PullRequest, 0, 2) + return prs, db.GetEngine(db.DefaultContext). + Where("base_repo_id=? AND base_branch=? AND has_merged=? AND issue.is_closed=?", + repoID, branch, false, false). + Join("INNER", "issue", "issue.id=pull_request.issue_id"). + Find(&prs) +} + +// GetPullRequestIDsByCheckStatus returns all pull requests according the special checking status. +func GetPullRequestIDsByCheckStatus(status PullRequestStatus) ([]int64, error) { + prs := make([]int64, 0, 10) + return prs, db.GetEngine(db.DefaultContext).Table("pull_request"). + Where("status=?", status). + Cols("pull_request.id"). + Find(&prs) +} + +// PullRequests returns all pull requests for a base Repo by the given conditions +func PullRequests(baseRepoID int64, opts *PullRequestsOptions) ([]*PullRequest, int64, error) { + if opts.Page <= 0 { + opts.Page = 1 + } + + countSession, err := listPullRequestStatement(baseRepoID, opts) + if err != nil { + log.Error("listPullRequestStatement: %v", err) + return nil, 0, err + } + maxResults, err := countSession.Count(new(PullRequest)) + if err != nil { + log.Error("Count PRs: %v", err) + return nil, maxResults, err + } + + findSession, err := listPullRequestStatement(baseRepoID, opts) + sortIssuesSession(findSession, opts.SortType, 0) + if err != nil { + log.Error("listPullRequestStatement: %v", err) + return nil, maxResults, err + } + findSession = db.SetSessionPagination(findSession, opts) + prs := make([]*PullRequest, 0, opts.PageSize) + return prs, maxResults, findSession.Find(&prs) +} + +// PullRequestList defines a list of pull requests +type PullRequestList []*PullRequest + +func (prs PullRequestList) loadAttributes(ctx context.Context) error { + if len(prs) == 0 { + return nil + } + + // Load issues. + issueIDs := prs.getIssueIDs() + issues := make([]*Issue, 0, len(issueIDs)) + if err := db.GetEngine(ctx). + Where("id > 0"). + In("id", issueIDs). + Find(&issues); err != nil { + return fmt.Errorf("find issues: %v", err) + } + + set := make(map[int64]*Issue) + for i := range issues { + set[issues[i].ID] = issues[i] + } + for i := range prs { + prs[i].Issue = set[prs[i].IssueID] + } + return nil +} + +func (prs PullRequestList) getIssueIDs() []int64 { + issueIDs := make([]int64, 0, len(prs)) + for i := range prs { + issueIDs = append(issueIDs, prs[i].IssueID) + } + return issueIDs +} + +// LoadAttributes load all the prs attributes +func (prs PullRequestList) LoadAttributes() error { + return prs.loadAttributes(db.DefaultContext) +} + +// InvalidateCodeComments will lookup the prs for code comments which got invalidated by change +func (prs PullRequestList) InvalidateCodeComments(ctx context.Context, doer *user_model.User, repo *git.Repository, branch string) error { + if len(prs) == 0 { + return nil + } + issueIDs := prs.getIssueIDs() + var codeComments []*Comment + if err := db.GetEngine(ctx). + Where("type = ? and invalidated = ?", CommentTypeCode, false). + In("issue_id", issueIDs). + Find(&codeComments); err != nil { + return fmt.Errorf("find code comments: %v", err) + } + for _, comment := range codeComments { + if err := comment.CheckInvalidation(repo, doer, branch); err != nil { + return err + } + } + return nil +} diff --git a/models/issues/pull_test.go b/models/issues/pull_test.go new file mode 100644 index 0000000000..0d1991383d --- /dev/null +++ b/models/issues/pull_test.go @@ -0,0 +1,277 @@ +// 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 ( + "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" +) + +func TestPullRequest_LoadAttributes(t *testing.T) { + assert.NoError(t, unittest.PrepareTestDatabase()) + 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) +} + +func TestPullRequest_LoadIssue(t *testing.T) { + assert.NoError(t, unittest.PrepareTestDatabase()) + 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) + assert.NoError(t, pr.LoadIssue()) + assert.NotNil(t, pr.Issue) + assert.Equal(t, int64(2), pr.Issue.ID) +} + +func TestPullRequest_LoadBaseRepo(t *testing.T) { + assert.NoError(t, unittest.PrepareTestDatabase()) + 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) + assert.NoError(t, pr.LoadBaseRepo()) + assert.NotNil(t, pr.BaseRepo) + assert.Equal(t, pr.BaseRepoID, pr.BaseRepo.ID) +} + +func TestPullRequest_LoadHeadRepo(t *testing.T) { + assert.NoError(t, unittest.PrepareTestDatabase()) + 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) +} + +// TODO TestMerge + +// TODO TestNewPullRequest + +func TestPullRequestsNewest(t *testing.T) { + assert.NoError(t, unittest.PrepareTestDatabase()) + prs, count, err := issues_model.PullRequests(1, &issues_model.PullRequestsOptions{ + ListOptions: db.ListOptions{ + Page: 1, + }, + State: "open", + SortType: "newest", + Labels: []string{}, + }) + assert.NoError(t, err) + assert.EqualValues(t, 3, count) + if assert.Len(t, prs, 3) { + assert.EqualValues(t, 5, prs[0].ID) + assert.EqualValues(t, 2, prs[1].ID) + assert.EqualValues(t, 1, prs[2].ID) + } +} + +func TestPullRequestsOldest(t *testing.T) { + assert.NoError(t, unittest.PrepareTestDatabase()) + prs, count, err := issues_model.PullRequests(1, &issues_model.PullRequestsOptions{ + ListOptions: db.ListOptions{ + Page: 1, + }, + State: "open", + SortType: "oldest", + Labels: []string{}, + }) + assert.NoError(t, err) + assert.EqualValues(t, 3, count) + if assert.Len(t, prs, 3) { + assert.EqualValues(t, 1, prs[0].ID) + assert.EqualValues(t, 2, prs[1].ID) + assert.EqualValues(t, 5, prs[2].ID) + } +} + +func TestGetUnmergedPullRequest(t *testing.T) { + assert.NoError(t, unittest.PrepareTestDatabase()) + pr, err := issues_model.GetUnmergedPullRequest(1, 1, "branch2", "master", issues_model.PullRequestFlowGithub) + assert.NoError(t, err) + assert.Equal(t, int64(2), pr.ID) + + _, err = issues_model.GetUnmergedPullRequest(1, 9223372036854775807, "branch1", "master", issues_model.PullRequestFlowGithub) + assert.Error(t, err) + assert.True(t, issues_model.IsErrPullRequestNotExist(err)) +} + +func TestHasUnmergedPullRequestsByHeadInfo(t *testing.T) { + assert.NoError(t, unittest.PrepareTestDatabase()) + + exist, err := issues_model.HasUnmergedPullRequestsByHeadInfo(db.DefaultContext, 1, "branch2") + assert.NoError(t, err) + assert.Equal(t, true, exist) + + 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 := issues_model.GetUnmergedPullRequestsByHeadInfo(1, "branch2") + assert.NoError(t, err) + assert.Len(t, prs, 1) + for _, pr := range prs { + assert.Equal(t, int64(1), pr.HeadRepoID) + assert.Equal(t, "branch2", pr.HeadBranch) + } +} + +func TestGetUnmergedPullRequestsByBaseInfo(t *testing.T) { + assert.NoError(t, unittest.PrepareTestDatabase()) + prs, err := issues_model.GetUnmergedPullRequestsByBaseInfo(1, "master") + assert.NoError(t, err) + assert.Len(t, prs, 1) + pr := prs[0] + assert.Equal(t, int64(2), pr.ID) + assert.Equal(t, int64(1), pr.BaseRepoID) + assert.Equal(t, "master", pr.BaseBranch) +} + +func TestGetPullRequestByIndex(t *testing.T) { + assert.NoError(t, unittest.PrepareTestDatabase()) + 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 = issues_model.GetPullRequestByIndex(db.DefaultContext, 9223372036854775807, 9223372036854775807) + assert.Error(t, err) + assert.True(t, issues_model.IsErrPullRequestNotExist(err)) + + _, err = issues_model.GetPullRequestByIndex(db.DefaultContext, 1, 0) + assert.Error(t, err) + assert.True(t, issues_model.IsErrPullRequestNotExist(err)) +} + +func TestGetPullRequestByID(t *testing.T) { + assert.NoError(t, unittest.PrepareTestDatabase()) + 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 = issues_model.GetPullRequestByID(db.DefaultContext, 9223372036854775807) + assert.Error(t, err) + assert.True(t, issues_model.IsErrPullRequestNotExist(err)) +} + +func TestGetPullRequestByIssueID(t *testing.T) { + assert.NoError(t, unittest.PrepareTestDatabase()) + pr, err := issues_model.GetPullRequestByIssueID(db.DefaultContext, 2) + assert.NoError(t, err) + assert.Equal(t, int64(2), pr.IssueID) + + _, err = issues_model.GetPullRequestByIssueID(db.DefaultContext, 9223372036854775807) + assert.Error(t, err) + assert.True(t, issues_model.IsErrPullRequestNotExist(err)) +} + +func TestPullRequest_Update(t *testing.T) { + assert.NoError(t, unittest.PrepareTestDatabase()) + pr := unittest.AssertExistsAndLoadBean(t, &issues_model.PullRequest{ID: 1}).(*issues_model.PullRequest) + pr.BaseBranch = "baseBranch" + pr.HeadBranch = "headBranch" + pr.Update() + + 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) +} + +func TestPullRequest_UpdateCols(t *testing.T) { + assert.NoError(t, unittest.PrepareTestDatabase()) + pr := &issues_model.PullRequest{ + ID: 1, + BaseBranch: "baseBranch", + HeadBranch: "headBranch", + } + assert.NoError(t, pr.UpdateCols("head_branch")) + + 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) +} + +func TestPullRequestList_LoadAttributes(t *testing.T) { + assert.NoError(t, unittest.PrepareTestDatabase()) + + 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, 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, issues_model.PullRequestList([]*issues_model.PullRequest{}).LoadAttributes()) +} + +// TODO TestAddTestPullRequestTask + +func TestPullRequest_IsWorkInProgress(t *testing.T) { + assert.NoError(t, unittest.PrepareTestDatabase()) + + pr := unittest.AssertExistsAndLoadBean(t, &issues_model.PullRequest{ID: 2}).(*issues_model.PullRequest) + pr.LoadIssue() + + assert.False(t, pr.IsWorkInProgress()) + + pr.Issue.Title = "WIP: " + pr.Issue.Title + assert.True(t, pr.IsWorkInProgress()) + + pr.Issue.Title = "[wip]: " + pr.Issue.Title + assert.True(t, pr.IsWorkInProgress()) +} + +func TestPullRequest_GetWorkInProgressPrefixWorkInProgress(t *testing.T) { + assert.NoError(t, unittest.PrepareTestDatabase()) + + pr := unittest.AssertExistsAndLoadBean(t, &issues_model.PullRequest{ID: 2}).(*issues_model.PullRequest) + pr.LoadIssue() + + assert.Empty(t, pr.GetWorkInProgressPrefix()) + + original := pr.Issue.Title + pr.Issue.Title = "WIP: " + original + assert.Equal(t, "WIP:", pr.GetWorkInProgressPrefix()) + + 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/issues/review.go b/models/issues/review.go new file mode 100644 index 0000000000..ee65bec3f8 --- /dev/null +++ b/models/issues/review.go @@ -0,0 +1,1018 @@ +// Copyright 2018 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 ( + "context" + "fmt" + "strings" + + "code.gitea.io/gitea/models/db" + git_model "code.gitea.io/gitea/models/git" + "code.gitea.io/gitea/models/organization" + "code.gitea.io/gitea/models/perm" + access_model "code.gitea.io/gitea/models/perm/access" + "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 + +// ReviewTypeUnknown unknown review type +const ReviewTypeUnknown ReviewType = -1 + +const ( + // ReviewTypePending is a review which is not published yet + ReviewTypePending ReviewType = iota + // ReviewTypeApprove approves changes + ReviewTypeApprove + // ReviewTypeComment gives general feedback + ReviewTypeComment + // ReviewTypeReject gives feedback blocking merge + ReviewTypeReject + // ReviewTypeRequest request review from others + ReviewTypeRequest +) + +// Icon returns the corresponding icon for the review type +func (rt ReviewType) Icon() string { + switch rt { + case ReviewTypeApprove: + return "check" + case ReviewTypeReject: + return "diff" + case ReviewTypeComment: + return "comment" + case ReviewTypeRequest: + return "dot-fill" + default: + return "comment" + } +} + +// Review represents collection of code comments giving feedback for a PR +type Review struct { + ID int64 `xorm:"pk autoincr"` + Type ReviewType + Reviewer *user_model.User `xorm:"-"` + ReviewerID int64 `xorm:"index"` + ReviewerTeamID int64 `xorm:"NOT NULL DEFAULT 0"` + ReviewerTeam *organization.Team `xorm:"-"` + OriginalAuthor string + OriginalAuthorID int64 + Issue *Issue `xorm:"-"` + IssueID int64 `xorm:"index"` + Content string `xorm:"TEXT"` + // Official is a review made by an assigned approver (counts towards approval) + Official bool `xorm:"NOT NULL DEFAULT false"` + CommitID string `xorm:"VARCHAR(40)"` + Stale bool `xorm:"NOT NULL DEFAULT false"` + Dismissed bool `xorm:"NOT NULL DEFAULT false"` + + CreatedUnix timeutil.TimeStamp `xorm:"INDEX created"` + UpdatedUnix timeutil.TimeStamp `xorm:"INDEX updated"` + + // CodeComments are the initial code comments of the review + CodeComments CodeComments `xorm:"-"` + + Comments []*Comment `xorm:"-"` +} + +func init() { + db.RegisterModel(new(Review)) +} + +// LoadCodeComments loads CodeComments +func (r *Review) LoadCodeComments(ctx context.Context) (err error) { + if r.CodeComments != nil { + return + } + if err = r.loadIssue(ctx); err != nil { + return + } + r.CodeComments, err = fetchCodeCommentsByReview(ctx, r.Issue, nil, r) + return +} + +func (r *Review) loadIssue(ctx context.Context) (err error) { + if r.Issue != nil { + return + } + r.Issue, err = GetIssueByID(ctx, r.IssueID) + return +} + +func (r *Review) loadReviewer(ctx context.Context) (err error) { + if r.ReviewerID == 0 || r.Reviewer != nil { + return + } + r.Reviewer, err = user_model.GetUserByIDCtx(ctx, r.ReviewerID) + return +} + +func (r *Review) loadReviewerTeam(ctx context.Context) (err error) { + if r.ReviewerTeamID == 0 || r.ReviewerTeam != nil { + return + } + + r.ReviewerTeam, err = organization.GetTeamByID(ctx, r.ReviewerTeamID) + return +} + +// LoadReviewer loads reviewer +func (r *Review) LoadReviewer() error { + return r.loadReviewer(db.DefaultContext) +} + +// LoadReviewerTeam loads reviewer team +func (r *Review) LoadReviewerTeam() error { + return r.loadReviewerTeam(db.DefaultContext) +} + +// LoadAttributes loads all attributes except CodeComments +func (r *Review) LoadAttributes(ctx context.Context) (err error) { + if err = r.loadIssue(ctx); err != nil { + return + } + if err = r.LoadCodeComments(ctx); err != nil { + return + } + if err = r.loadReviewer(ctx); err != nil { + return + } + if err = r.loadReviewerTeam(ctx); err != nil { + return + } + return +} + +// GetReviewByID returns the review by the given ID +func GetReviewByID(ctx context.Context, id int64) (*Review, error) { + review := new(Review) + if has, err := db.GetEngine(ctx).ID(id).Get(review); err != nil { + return nil, err + } else if !has { + return nil, ErrReviewNotExist{ID: id} + } else { + return review, nil + } +} + +// FindReviewOptions represent possible filters to find reviews +type FindReviewOptions struct { + db.ListOptions + Type ReviewType + IssueID int64 + ReviewerID int64 + OfficialOnly bool +} + +func (opts *FindReviewOptions) toCond() builder.Cond { + cond := builder.NewCond() + if opts.IssueID > 0 { + cond = cond.And(builder.Eq{"issue_id": opts.IssueID}) + } + if opts.ReviewerID > 0 { + cond = cond.And(builder.Eq{"reviewer_id": opts.ReviewerID}) + } + if opts.Type != ReviewTypeUnknown { + cond = cond.And(builder.Eq{"type": opts.Type}) + } + if opts.OfficialOnly { + cond = cond.And(builder.Eq{"official": true}) + } + return cond +} + +// FindReviews returns reviews passing FindReviewOptions +func FindReviews(ctx context.Context, opts FindReviewOptions) ([]*Review, error) { + reviews := make([]*Review, 0, 10) + sess := db.GetEngine(ctx).Where(opts.toCond()) + if opts.Page > 0 { + sess = db.SetSessionPagination(sess, &opts) + } + return reviews, sess. + Asc("created_unix"). + Asc("id"). + Find(&reviews) +} + +// CountReviews returns count of reviews passing FindReviewOptions +func CountReviews(opts FindReviewOptions) (int64, error) { + return db.GetEngine(db.DefaultContext).Where(opts.toCond()).Count(&Review{}) +} + +// CreateReviewOptions represent the options to create a review. Type, Issue and Reviewer are required. +type CreateReviewOptions struct { + Content string + Type ReviewType + Issue *Issue + Reviewer *user_model.User + ReviewerTeam *organization.Team + Official bool + CommitID string + Stale bool +} + +// IsOfficialReviewer check if at least one of the provided reviewers can make official reviews in issue (counts towards required approvals) +func IsOfficialReviewer(ctx context.Context, issue *Issue, reviewers ...*user_model.User) (bool, error) { + pr, err := GetPullRequestByIssueID(ctx, issue.ID) + if err != nil { + return false, err + } + if err = pr.LoadProtectedBranchCtx(ctx); err != nil { + return false, err + } + if pr.ProtectedBranch == nil { + return false, nil + } + + for _, reviewer := range reviewers { + official, err := git_model.IsUserOfficialReviewerCtx(ctx, pr.ProtectedBranch, reviewer) + if official || err != nil { + return official, err + } + } + + return false, nil +} + +// IsOfficialReviewerTeam check if reviewer in this team can make official reviews in issue (counts towards required approvals) +func IsOfficialReviewerTeam(ctx context.Context, issue *Issue, team *organization.Team) (bool, error) { + pr, err := GetPullRequestByIssueID(ctx, issue.ID) + if err != nil { + return false, err + } + if err = pr.LoadProtectedBranchCtx(ctx); err != nil { + return false, err + } + if pr.ProtectedBranch == nil { + return false, nil + } + + if !pr.ProtectedBranch.EnableApprovalsWhitelist { + return team.UnitAccessModeCtx(ctx, unit.TypeCode) >= perm.AccessModeWrite, nil + } + + return base.Int64sContains(pr.ProtectedBranch.ApprovalsWhitelistTeamIDs, team.ID), nil +} + +// CreateReview creates a new review based on opts +func CreateReview(ctx context.Context, opts CreateReviewOptions) (*Review, error) { + review := &Review{ + Type: opts.Type, + Issue: opts.Issue, + IssueID: opts.Issue.ID, + Reviewer: opts.Reviewer, + ReviewerTeam: opts.ReviewerTeam, + Content: opts.Content, + Official: opts.Official, + CommitID: opts.CommitID, + Stale: opts.Stale, + } + if opts.Reviewer != nil { + review.ReviewerID = opts.Reviewer.ID + } else { + if review.Type != ReviewTypeRequest { + review.Type = ReviewTypeRequest + } + review.ReviewerTeamID = opts.ReviewerTeam.ID + } + return review, db.Insert(ctx, review) +} + +// GetCurrentReview returns the current pending review of reviewer for given issue +func GetCurrentReview(ctx context.Context, reviewer *user_model.User, issue *Issue) (*Review, error) { + if reviewer == nil { + return nil, nil + } + reviews, err := FindReviews(ctx, FindReviewOptions{ + Type: ReviewTypePending, + IssueID: issue.ID, + ReviewerID: reviewer.ID, + }) + if err != nil { + return nil, err + } + if len(reviews) == 0 { + return nil, ErrReviewNotExist{} + } + reviews[0].Reviewer = reviewer + reviews[0].Issue = issue + return reviews[0], nil +} + +// ReviewExists returns whether a review exists for a particular line of code in the PR +func ReviewExists(issue *Issue, treePath string, line int64) (bool, error) { + return db.GetEngine(db.DefaultContext).Cols("id").Exist(&Comment{IssueID: issue.ID, TreePath: treePath, Line: line, Type: CommentTypeCode}) +} + +// ContentEmptyErr represents an content empty error +type ContentEmptyErr struct{} + +func (ContentEmptyErr) Error() string { + return "Review content is empty" +} + +// IsContentEmptyErr returns true if err is a ContentEmptyErr +func IsContentEmptyErr(err error) bool { + _, ok := err.(ContentEmptyErr) + return ok +} + +// SubmitReview creates a review out of the existing pending review or creates a new one if no pending review exist +func SubmitReview(doer *user_model.User, issue *Issue, reviewType ReviewType, content, commitID string, stale bool, attachmentUUIDs []string) (*Review, *Comment, error) { + ctx, committer, err := db.TxContext() + if err != nil { + return nil, nil, err + } + defer committer.Close() + sess := db.GetEngine(ctx) + + official := false + + review, err := GetCurrentReview(ctx, doer, issue) + if err != nil { + if !IsErrReviewNotExist(err) { + return nil, nil, err + } + + if reviewType != ReviewTypeApprove && len(strings.TrimSpace(content)) == 0 { + return nil, nil, ContentEmptyErr{} + } + + if reviewType == ReviewTypeApprove || reviewType == ReviewTypeReject { + // Only reviewers latest review of type approve and reject shall count as "official", so existing reviews needs to be cleared + if _, err := db.Exec(ctx, "UPDATE `review` SET official=? WHERE issue_id=? AND reviewer_id=?", false, issue.ID, doer.ID); err != nil { + return nil, nil, err + } + if official, err = IsOfficialReviewer(ctx, issue, doer); err != nil { + return nil, nil, err + } + } + + // No current review. Create a new one! + if review, err = CreateReview(ctx, CreateReviewOptions{ + Type: reviewType, + Issue: issue, + Reviewer: doer, + Content: content, + Official: official, + CommitID: commitID, + Stale: stale, + }); err != nil { + return nil, nil, err + } + } else { + if err := review.LoadCodeComments(ctx); err != nil { + return nil, nil, err + } + if reviewType != ReviewTypeApprove && len(review.CodeComments) == 0 && len(strings.TrimSpace(content)) == 0 { + return nil, nil, ContentEmptyErr{} + } + + if reviewType == ReviewTypeApprove || reviewType == ReviewTypeReject { + // Only reviewers latest review of type approve and reject shall count as "official", so existing reviews needs to be cleared + if _, err := db.Exec(ctx, "UPDATE `review` SET official=? WHERE issue_id=? AND reviewer_id=?", false, issue.ID, doer.ID); err != nil { + return nil, nil, err + } + if official, err = IsOfficialReviewer(ctx, issue, doer); err != nil { + return nil, nil, err + } + } + + review.Official = official + review.Issue = issue + review.Content = content + review.Type = reviewType + review.CommitID = commitID + review.Stale = stale + + if _, err := sess.ID(review.ID).Cols("content, type, official, commit_id, stale").Update(review); err != nil { + return nil, nil, err + } + } + + comm, err := CreateCommentCtx(ctx, &CreateCommentOptions{ + Type: CommentTypeReview, + Doer: doer, + Content: review.Content, + Issue: issue, + Repo: issue.Repo, + ReviewID: review.ID, + Attachments: attachmentUUIDs, + }) + if err != nil || comm == nil { + return nil, nil, err + } + + // try to remove team review request if need + if issue.Repo.Owner.IsOrganization() && (reviewType == ReviewTypeApprove || reviewType == ReviewTypeReject) { + teamReviewRequests := make([]*Review, 0, 10) + if err := sess.SQL("SELECT * FROM review WHERE issue_id = ? AND reviewer_team_id > 0 AND type = ?", issue.ID, ReviewTypeRequest).Find(&teamReviewRequests); err != nil { + return nil, nil, err + } + + for _, teamReviewRequest := range teamReviewRequests { + ok, err := organization.IsTeamMember(ctx, issue.Repo.OwnerID, teamReviewRequest.ReviewerTeamID, doer.ID) + if err != nil { + return nil, nil, err + } else if !ok { + continue + } + + if _, err := sess.ID(teamReviewRequest.ID).NoAutoCondition().Delete(teamReviewRequest); err != nil { + return nil, nil, err + } + } + } + + comm.Review = review + return review, comm, committer.Commit() +} + +// GetReviewersByIssueID gets the latest review of each reviewer for a pull request +func GetReviewersByIssueID(issueID int64) ([]*Review, error) { + reviews := make([]*Review, 0, 10) + + sess := db.GetEngine(db.DefaultContext) + + // Get latest review of each reviewer, sorted in order they were made + if err := sess.SQL("SELECT * FROM review WHERE id IN (SELECT max(id) as id FROM review WHERE issue_id = ? AND reviewer_team_id = 0 AND type in (?, ?, ?) AND dismissed = ? AND original_author_id = 0 GROUP BY issue_id, reviewer_id) ORDER BY review.updated_unix ASC", + issueID, ReviewTypeApprove, ReviewTypeReject, ReviewTypeRequest, false). + Find(&reviews); err != nil { + return nil, err + } + + teamReviewRequests := make([]*Review, 0, 5) + if err := sess.SQL("SELECT * FROM review WHERE id IN (SELECT max(id) as id FROM review WHERE issue_id = ? AND reviewer_team_id <> 0 AND original_author_id = 0 GROUP BY issue_id, reviewer_team_id) ORDER BY review.updated_unix ASC", + issueID). + Find(&teamReviewRequests); err != nil { + return nil, err + } + + if len(teamReviewRequests) > 0 { + reviews = append(reviews, teamReviewRequests...) + } + + return reviews, nil +} + +// GetReviewersFromOriginalAuthorsByIssueID gets the latest review of each original authors for a pull request +func GetReviewersFromOriginalAuthorsByIssueID(issueID int64) ([]*Review, error) { + reviews := make([]*Review, 0, 10) + + // Get latest review of each reviewer, sorted in order they were made + if err := db.GetEngine(db.DefaultContext).SQL("SELECT * FROM review WHERE id IN (SELECT max(id) as id FROM review WHERE issue_id = ? AND reviewer_team_id = 0 AND type in (?, ?, ?) AND original_author_id <> 0 GROUP BY issue_id, original_author_id) ORDER BY review.updated_unix ASC", + issueID, ReviewTypeApprove, ReviewTypeReject, ReviewTypeRequest). + Find(&reviews); err != nil { + return nil, err + } + + return reviews, nil +} + +// GetReviewByIssueIDAndUserID get the latest review of reviewer for a pull request +func GetReviewByIssueIDAndUserID(ctx context.Context, issueID, userID int64) (*Review, error) { + review := new(Review) + + has, err := db.GetEngine(ctx).SQL("SELECT * FROM review WHERE id IN (SELECT max(id) as id FROM review WHERE issue_id = ? AND reviewer_id = ? AND original_author_id = 0 AND type in (?, ?, ?))", + issueID, userID, ReviewTypeApprove, ReviewTypeReject, ReviewTypeRequest). + Get(review) + if err != nil { + return nil, err + } + + if !has { + return nil, ErrReviewNotExist{} + } + + return review, nil +} + +// GetTeamReviewerByIssueIDAndTeamID get the latest review request of reviewer team for a pull request +func GetTeamReviewerByIssueIDAndTeamID(ctx context.Context, issueID, teamID int64) (review *Review, err error) { + review = new(Review) + + has := false + if has, err = db.GetEngine(ctx).SQL("SELECT * FROM review WHERE id IN (SELECT max(id) as id FROM review WHERE issue_id = ? AND reviewer_team_id = ?)", + issueID, teamID). + Get(review); err != nil { + return nil, err + } + + if !has { + return nil, ErrReviewNotExist{0} + } + + return +} + +// MarkReviewsAsStale marks existing reviews as stale +func MarkReviewsAsStale(issueID int64) (err error) { + _, err = db.GetEngine(db.DefaultContext).Exec("UPDATE `review` SET stale=? WHERE issue_id=?", true, issueID) + + return +} + +// MarkReviewsAsNotStale marks existing reviews as not stale for a giving commit SHA +func MarkReviewsAsNotStale(issueID int64, commitID string) (err error) { + _, err = db.GetEngine(db.DefaultContext).Exec("UPDATE `review` SET stale=? WHERE issue_id=? AND commit_id=?", false, issueID, commitID) + + return +} + +// DismissReview change the dismiss status of a review +func DismissReview(review *Review, isDismiss bool) (err error) { + if review.Dismissed == isDismiss || (review.Type != ReviewTypeApprove && review.Type != ReviewTypeReject) { + return nil + } + + review.Dismissed = isDismiss + + if review.ID == 0 { + return ErrReviewNotExist{} + } + + _, err = db.GetEngine(db.DefaultContext).ID(review.ID).Cols("dismissed").Update(review) + + return +} + +// InsertReviews inserts review and review comments +func InsertReviews(reviews []*Review) error { + ctx, committer, err := db.TxContext() + if err != nil { + return err + } + defer committer.Close() + sess := db.GetEngine(ctx) + + for _, review := range reviews { + if _, err := sess.NoAutoTime().Insert(review); err != nil { + return err + } + + if _, err := sess.NoAutoTime().Insert(&Comment{ + Type: CommentTypeReview, + Content: review.Content, + PosterID: review.ReviewerID, + OriginalAuthor: review.OriginalAuthor, + OriginalAuthorID: review.OriginalAuthorID, + IssueID: review.IssueID, + ReviewID: review.ID, + CreatedUnix: review.CreatedUnix, + UpdatedUnix: review.UpdatedUnix, + }); err != nil { + return err + } + + for _, c := range review.Comments { + c.ReviewID = review.ID + } + + if len(review.Comments) > 0 { + if _, err := sess.NoAutoTime().Insert(review.Comments); err != nil { + return err + } + } + } + + return committer.Commit() +} + +// AddReviewRequest add a review request from one reviewer +func AddReviewRequest(issue *Issue, reviewer, doer *user_model.User) (*Comment, error) { + ctx, committer, err := db.TxContext() + if err != nil { + return nil, err + } + defer committer.Close() + sess := db.GetEngine(ctx) + + review, err := GetReviewByIssueIDAndUserID(ctx, issue.ID, reviewer.ID) + if err != nil && !IsErrReviewNotExist(err) { + return nil, err + } + + // skip it when reviewer hase been request to review + if review != nil && review.Type == ReviewTypeRequest { + return nil, nil + } + + official, err := IsOfficialReviewer(ctx, issue, reviewer, doer) + if err != nil { + return nil, err + } else if official { + if _, err := sess.Exec("UPDATE `review` SET official=? WHERE issue_id=? AND reviewer_id=?", false, issue.ID, reviewer.ID); err != nil { + return nil, err + } + } + + review, err = CreateReview(ctx, CreateReviewOptions{ + Type: ReviewTypeRequest, + Issue: issue, + Reviewer: reviewer, + Official: official, + Stale: false, + }) + if err != nil { + return nil, err + } + + comment, err := CreateCommentCtx(ctx, &CreateCommentOptions{ + Type: CommentTypeReviewRequest, + Doer: doer, + Repo: issue.Repo, + Issue: issue, + RemovedAssignee: false, // Use RemovedAssignee as !isRequest + AssigneeID: reviewer.ID, // Use AssigneeID as reviewer ID + ReviewID: review.ID, + }) + if err != nil { + return nil, err + } + + return comment, committer.Commit() +} + +// RemoveReviewRequest remove a review request from one reviewer +func RemoveReviewRequest(issue *Issue, reviewer, doer *user_model.User) (*Comment, error) { + ctx, committer, err := db.TxContext() + if err != nil { + return nil, err + } + defer committer.Close() + + review, err := GetReviewByIssueIDAndUserID(ctx, issue.ID, reviewer.ID) + if err != nil && !IsErrReviewNotExist(err) { + return nil, err + } + + if review == nil || review.Type != ReviewTypeRequest { + return nil, nil + } + + if _, err = db.DeleteByBean(ctx, review); err != nil { + return nil, err + } + + official, err := IsOfficialReviewer(ctx, issue, reviewer) + if err != nil { + return nil, err + } else if official { + // recalculate the latest official review for reviewer + review, err := GetReviewByIssueIDAndUserID(ctx, issue.ID, reviewer.ID) + if err != nil && !IsErrReviewNotExist(err) { + return nil, err + } + + if review != nil { + if _, err := db.Exec(ctx, "UPDATE `review` SET official=? WHERE id=?", true, review.ID); err != nil { + return nil, err + } + } + } + + comment, err := CreateCommentCtx(ctx, &CreateCommentOptions{ + Type: CommentTypeReviewRequest, + Doer: doer, + Repo: issue.Repo, + Issue: issue, + RemovedAssignee: true, // Use RemovedAssignee as !isRequest + AssigneeID: reviewer.ID, // Use AssigneeID as reviewer ID + }) + if err != nil { + return nil, err + } + + return comment, committer.Commit() +} + +// AddTeamReviewRequest add a review request from one team +func AddTeamReviewRequest(issue *Issue, reviewer *organization.Team, doer *user_model.User) (*Comment, error) { + ctx, committer, err := db.TxContext() + if err != nil { + return nil, err + } + defer committer.Close() + + review, err := GetTeamReviewerByIssueIDAndTeamID(ctx, issue.ID, reviewer.ID) + if err != nil && !IsErrReviewNotExist(err) { + return nil, err + } + + // This team already has been requested to review - therefore skip this. + if review != nil { + return nil, nil + } + + official, err := IsOfficialReviewerTeam(ctx, issue, reviewer) + if err != nil { + return nil, fmt.Errorf("isOfficialReviewerTeam(): %v", err) + } else if !official { + if official, err = IsOfficialReviewer(ctx, issue, doer); err != nil { + return nil, fmt.Errorf("isOfficialReviewer(): %v", err) + } + } + + if review, err = CreateReview(ctx, CreateReviewOptions{ + Type: ReviewTypeRequest, + Issue: issue, + ReviewerTeam: reviewer, + Official: official, + Stale: false, + }); err != nil { + return nil, err + } + + if official { + if _, err := db.Exec(ctx, "UPDATE `review` SET official=? WHERE issue_id=? AND reviewer_team_id=?", false, issue.ID, reviewer.ID); err != nil { + return nil, err + } + } + + comment, err := CreateCommentCtx(ctx, &CreateCommentOptions{ + Type: CommentTypeReviewRequest, + Doer: doer, + Repo: issue.Repo, + Issue: issue, + RemovedAssignee: false, // Use RemovedAssignee as !isRequest + AssigneeTeamID: reviewer.ID, // Use AssigneeTeamID as reviewer team ID + ReviewID: review.ID, + }) + if err != nil { + return nil, fmt.Errorf("CreateCommentCtx(): %v", err) + } + + return comment, committer.Commit() +} + +// RemoveTeamReviewRequest remove a review request from one team +func RemoveTeamReviewRequest(issue *Issue, reviewer *organization.Team, doer *user_model.User) (*Comment, error) { + ctx, committer, err := db.TxContext() + if err != nil { + return nil, err + } + defer committer.Close() + + review, err := GetTeamReviewerByIssueIDAndTeamID(ctx, issue.ID, reviewer.ID) + if err != nil && !IsErrReviewNotExist(err) { + return nil, err + } + + if review == nil { + return nil, nil + } + + if _, err = db.DeleteByBean(ctx, review); err != nil { + return nil, err + } + + official, err := IsOfficialReviewerTeam(ctx, issue, reviewer) + if err != nil { + return nil, fmt.Errorf("isOfficialReviewerTeam(): %v", err) + } + + if official { + // recalculate which is the latest official review from that team + review, err := GetReviewByIssueIDAndUserID(ctx, issue.ID, -reviewer.ID) + if err != nil && !IsErrReviewNotExist(err) { + return nil, err + } + + if review != nil { + if _, err := db.Exec(ctx, "UPDATE `review` SET official=? WHERE id=?", true, review.ID); err != nil { + return nil, err + } + } + } + + if doer == nil { + return nil, committer.Commit() + } + + comment, err := CreateCommentCtx(ctx, &CreateCommentOptions{ + Type: CommentTypeReviewRequest, + Doer: doer, + Repo: issue.Repo, + Issue: issue, + RemovedAssignee: true, // Use RemovedAssignee as !isRequest + AssigneeTeamID: reviewer.ID, // Use AssigneeTeamID as reviewer team ID + }) + if err != nil { + return nil, fmt.Errorf("CreateCommentCtx(): %v", err) + } + + return comment, committer.Commit() +} + +// MarkConversation Add or remove Conversation mark for a code comment +func MarkConversation(comment *Comment, doer *user_model.User, isResolve bool) (err error) { + if comment.Type != CommentTypeCode { + return nil + } + + if isResolve { + if comment.ResolveDoerID != 0 { + return nil + } + + if _, err = db.GetEngine(db.DefaultContext).Exec("UPDATE `comment` SET resolve_doer_id=? WHERE id=?", doer.ID, comment.ID); err != nil { + return err + } + } else { + if comment.ResolveDoerID == 0 { + return nil + } + + if _, err = db.GetEngine(db.DefaultContext).Exec("UPDATE `comment` SET resolve_doer_id=? WHERE id=?", 0, comment.ID); err != nil { + return err + } + } + + return nil +} + +// CanMarkConversation Add or remove Conversation mark for a code comment permission check +// the PR writer , offfcial reviewer and poster can do it +func CanMarkConversation(issue *Issue, doer *user_model.User) (permResult bool, err error) { + if doer == nil || issue == nil { + return false, fmt.Errorf("issue or doer is nil") + } + + if doer.ID != issue.PosterID { + if err = issue.LoadRepo(db.DefaultContext); err != nil { + return false, err + } + + p, err := access_model.GetUserRepoPermission(db.DefaultContext, issue.Repo, doer) + if err != nil { + return false, err + } + + permResult = p.CanAccess(perm.AccessModeWrite, unit.TypePullRequests) + if !permResult { + if permResult, err = IsOfficialReviewer(db.DefaultContext, issue, doer); err != nil { + return false, err + } + } + + if !permResult { + return false, nil + } + } + + return true, nil +} + +// DeleteReview delete a review and it's code comments +func DeleteReview(r *Review) error { + ctx, committer, err := db.TxContext() + if err != nil { + return err + } + defer committer.Close() + sess := db.GetEngine(ctx) + + if r.ID == 0 { + return fmt.Errorf("review is not allowed to be 0") + } + + if r.Type == ReviewTypeRequest { + return fmt.Errorf("review request can not be deleted using this method") + } + + opts := FindCommentsOptions{ + Type: CommentTypeCode, + IssueID: r.IssueID, + ReviewID: r.ID, + } + + if _, err := sess.Where(opts.toConds()).Delete(new(Comment)); err != nil { + return err + } + + opts = FindCommentsOptions{ + Type: CommentTypeReview, + IssueID: r.IssueID, + ReviewID: r.ID, + } + + if _, err := sess.Where(opts.toConds()).Delete(new(Comment)); err != nil { + return err + } + + if _, err := sess.ID(r.ID).Delete(new(Review)); err != nil { + return err + } + + return committer.Commit() +} + +// GetCodeCommentsCount return count of CodeComments a Review has +func (r *Review) GetCodeCommentsCount() int { + opts := FindCommentsOptions{ + Type: CommentTypeCode, + IssueID: r.IssueID, + ReviewID: r.ID, + } + conds := opts.toConds() + if r.ID == 0 { + conds = conds.And(builder.Eq{"invalidated": false}) + } + + count, err := db.GetEngine(db.DefaultContext).Where(conds).Count(new(Comment)) + if err != nil { + return 0 + } + return int(count) +} + +// HTMLURL formats a URL-string to the related review issue-comment +func (r *Review) HTMLURL() string { + opts := FindCommentsOptions{ + Type: CommentTypeReview, + IssueID: r.IssueID, + ReviewID: r.ID, + } + comment := new(Comment) + has, err := db.GetEngine(db.DefaultContext).Where(opts.toConds()).Get(comment) + if err != nil || !has { + return "" + } + return comment.HTMLURL() +} + +// RemapExternalUser ExternalUserRemappable interface +func (r *Review) RemapExternalUser(externalName string, externalID, userID int64) error { + r.OriginalAuthor = externalName + r.OriginalAuthorID = externalID + r.ReviewerID = userID + return nil +} + +// GetUserID ExternalUserRemappable interface +func (r *Review) GetUserID() int64 { return r.ReviewerID } + +// GetExternalName ExternalUserRemappable interface +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/issues/review_test.go b/models/issues/review_test.go new file mode 100644 index 0000000000..3506604b46 --- /dev/null +++ b/models/issues/review_test.go @@ -0,0 +1,203 @@ +// 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" + + "github.com/stretchr/testify/assert" +) + +func TestGetReviewByID(t *testing.T) { + assert.NoError(t, unittest.PrepareTestDatabase()) + review, err := issues_model.GetReviewByID(db.DefaultContext, 1) + assert.NoError(t, err) + assert.Equal(t, "Demo Review", review.Content) + assert.Equal(t, issues_model.ReviewTypeApprove, review.Type) + + _, err = issues_model.GetReviewByID(db.DefaultContext, 23892) + assert.Error(t, err) + assert.True(t, issues_model.IsErrReviewNotExist(err), "IsErrReviewNotExist") +} + +func TestReview_LoadAttributes(t *testing.T) { + assert.NoError(t, unittest.PrepareTestDatabase()) + 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, &issues_model.Review{ID: 2}).(*issues_model.Review) + assert.Error(t, invalidReview1.LoadAttributes(db.DefaultContext)) + + 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, &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) + assert.Equal(t, int64(4), review.CodeComments["README.md"][int64(4)][0].Line) +} + +func TestReviewType_Icon(t *testing.T) { + 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 := issues_model.FindReviews(db.DefaultContext, issues_model.FindReviewOptions{ + Type: issues_model.ReviewTypeApprove, + IssueID: 2, + ReviewerID: 1, + }) + assert.NoError(t, err) + assert.Len(t, reviews, 1) + assert.Equal(t, "Demo Review", reviews[0].Content) +} + +func TestGetCurrentReview(t *testing.T) { + assert.NoError(t, unittest.PrepareTestDatabase()) + 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 := issues_model.GetCurrentReview(db.DefaultContext, user, issue) + assert.NoError(t, err) + assert.NotNil(t, review) + 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 := issues_model.GetCurrentReview(db.DefaultContext, user2, issue) + assert.Error(t, 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, &issues_model.Issue{ID: 2}).(*issues_model.Issue) + user := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 1}).(*user_model.User) + + review, err := issues_model.CreateReview(db.DefaultContext, issues_model.CreateReviewOptions{ + Content: "New Review", + Type: issues_model.ReviewTypePending, + Issue: issue, + Reviewer: user, + }) + assert.NoError(t, err) + assert.Equal(t, "New Review", review.Content) + unittest.AssertExistsAndLoadBean(t, &issues_model.Review{Content: "New Review"}) +} + +func TestGetReviewersByIssueID(t *testing.T) { + assert.NoError(t, unittest.PrepareTestDatabase()) + + 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 := []*issues_model.Review{} + expectedReviews = append(expectedReviews, + &issues_model.Review{ + Reviewer: user3, + Type: issues_model.ReviewTypeReject, + UpdatedUnix: 946684812, + }, + &issues_model.Review{ + Reviewer: user4, + Type: issues_model.ReviewTypeApprove, + UpdatedUnix: 946684813, + }, + &issues_model.Review{ + Reviewer: user2, + Type: issues_model.ReviewTypeReject, + UpdatedUnix: 946684814, + }) + + allReviews, err := issues_model.GetReviewersByIssueID(issue.ID) + for _, reviewer := range allReviews { + assert.NoError(t, reviewer.LoadReviewer()) + } + assert.NoError(t, err) + if assert.Len(t, allReviews, 3) { + for i, review := range allReviews { + assert.Equal(t, expectedReviews[i].Reviewer, review.Reviewer) + assert.Equal(t, expectedReviews[i].Type, review.Type) + assert.Equal(t, expectedReviews[i].UpdatedUnix, review.UpdatedUnix) + } + } +} + +func TestDismissReview(t *testing.T) { + assert.NoError(t, unittest.PrepareTestDatabase()) + + 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, 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, 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, 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, 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, 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, issues_model.DismissReview(rejectReviewExample, false)) + assert.False(t, rejectReviewExample.Dismissed) + assert.False(t, requestReviewExample.Dismissed) + assert.False(t, approveReviewExample.Dismissed) + + 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/issues/stopwatch.go b/models/issues/stopwatch.go new file mode 100644 index 0000000000..e7ac1314e9 --- /dev/null +++ b/models/issues/stopwatch.go @@ -0,0 +1,293 @@ +// 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 ( + "context" + "fmt" + "time" + + "code.gitea.io/gitea/models/db" + user_model "code.gitea.io/gitea/models/user" + "code.gitea.io/gitea/modules/timeutil" + "code.gitea.io/gitea/modules/util" +) + +// ErrIssueStopwatchNotExist represents an error that stopwatch is not exist +type ErrIssueStopwatchNotExist struct { + UserID int64 + IssueID int64 +} + +func (err ErrIssueStopwatchNotExist) Error() string { + return fmt.Sprintf("issue stopwatch doesn't exist[uid: %d, issue_id: %d", err.UserID, err.IssueID) +} + +// ErrIssueStopwatchAlreadyExist represents an error that stopwatch is already exist +type ErrIssueStopwatchAlreadyExist struct { + UserID int64 + IssueID int64 +} + +func (err ErrIssueStopwatchAlreadyExist) Error() string { + return fmt.Sprintf("issue stopwatch already exists[uid: %d, issue_id: %d", err.UserID, err.IssueID) +} + +// Stopwatch represents a stopwatch for time tracking. +type Stopwatch struct { + ID int64 `xorm:"pk autoincr"` + IssueID int64 `xorm:"INDEX"` + UserID int64 `xorm:"INDEX"` + CreatedUnix timeutil.TimeStamp `xorm:"created"` +} + +func init() { + db.RegisterModel(new(Stopwatch)) +} + +// Seconds returns the amount of time passed since creation, based on local server time +func (s Stopwatch) Seconds() int64 { + return int64(timeutil.TimeStampNow() - s.CreatedUnix) +} + +// Duration returns a human-readable duration string based on local server time +func (s Stopwatch) Duration() string { + return util.SecToTime(s.Seconds()) +} + +func getStopwatch(ctx context.Context, userID, issueID int64) (sw *Stopwatch, exists bool, err error) { + sw = new(Stopwatch) + exists, err = db.GetEngine(ctx). + Where("user_id = ?", userID). + And("issue_id = ?", issueID). + Get(sw) + return +} + +// UserIDCount is a simple coalition of UserID and Count +type UserStopwatch struct { + UserID int64 + StopWatches []*Stopwatch +} + +// GetUIDsAndNotificationCounts between the two provided times +func GetUIDsAndStopwatch() ([]*UserStopwatch, error) { + sws := []*Stopwatch{} + if err := db.GetEngine(db.DefaultContext).Where("issue_id != 0").Find(&sws); err != nil { + return nil, err + } + if len(sws) == 0 { + return []*UserStopwatch{}, nil + } + + lastUserID := int64(-1) + res := []*UserStopwatch{} + for _, sw := range sws { + if lastUserID == sw.UserID { + lastUserStopwatch := res[len(res)-1] + lastUserStopwatch.StopWatches = append(lastUserStopwatch.StopWatches, sw) + } else { + res = append(res, &UserStopwatch{ + UserID: sw.UserID, + StopWatches: []*Stopwatch{sw}, + }) + } + } + return res, nil +} + +// GetUserStopwatches return list of all stopwatches of a user +func GetUserStopwatches(userID int64, listOptions db.ListOptions) ([]*Stopwatch, error) { + sws := make([]*Stopwatch, 0, 8) + sess := db.GetEngine(db.DefaultContext).Where("stopwatch.user_id = ?", userID) + if listOptions.Page != 0 { + sess = db.SetSessionPagination(sess, &listOptions) + } + + err := sess.Find(&sws) + if err != nil { + return nil, err + } + return sws, nil +} + +// CountUserStopwatches return count of all stopwatches of a user +func CountUserStopwatches(userID int64) (int64, error) { + return db.GetEngine(db.DefaultContext).Where("user_id = ?", userID).Count(&Stopwatch{}) +} + +// StopwatchExists returns true if the stopwatch exists +func StopwatchExists(userID, issueID int64) bool { + _, exists, _ := getStopwatch(db.DefaultContext, userID, issueID) + return exists +} + +// HasUserStopwatch returns true if the user has a stopwatch +func HasUserStopwatch(ctx context.Context, userID int64) (exists bool, sw *Stopwatch, err error) { + sw = new(Stopwatch) + exists, err = db.GetEngine(ctx). + Where("user_id = ?", userID). + Get(sw) + return +} + +// FinishIssueStopwatchIfPossible if stopwatch exist then finish it otherwise ignore +func FinishIssueStopwatchIfPossible(ctx context.Context, user *user_model.User, issue *Issue) error { + _, exists, err := getStopwatch(ctx, user.ID, issue.ID) + if err != nil { + return err + } + if !exists { + return nil + } + return FinishIssueStopwatch(ctx, user, issue) +} + +// CreateOrStopIssueStopwatch create an issue stopwatch if it's not exist, otherwise finish it +func CreateOrStopIssueStopwatch(user *user_model.User, issue *Issue) error { + _, exists, err := getStopwatch(db.DefaultContext, user.ID, issue.ID) + if err != nil { + return err + } + if exists { + return FinishIssueStopwatch(db.DefaultContext, user, issue) + } + return CreateIssueStopwatch(db.DefaultContext, user, issue) +} + +// FinishIssueStopwatch if stopwatch exist then finish it otherwise return an error +func FinishIssueStopwatch(ctx context.Context, user *user_model.User, issue *Issue) error { + sw, exists, err := getStopwatch(ctx, user.ID, issue.ID) + if err != nil { + return err + } + if !exists { + return ErrIssueStopwatchNotExist{ + UserID: user.ID, + IssueID: issue.ID, + } + } + + // Create tracked time out of the time difference between start date and actual date + timediff := time.Now().Unix() - int64(sw.CreatedUnix) + + // Create TrackedTime + tt := &TrackedTime{ + Created: time.Now(), + IssueID: issue.ID, + UserID: user.ID, + Time: timediff, + } + + if err := db.Insert(ctx, tt); err != nil { + return err + } + + if err := issue.LoadRepo(ctx); err != nil { + return err + } + + if _, err := CreateCommentCtx(ctx, &CreateCommentOptions{ + Doer: user, + Issue: issue, + Repo: issue.Repo, + Content: util.SecToTime(timediff), + Type: CommentTypeStopTracking, + TimeID: tt.ID, + }); err != nil { + return err + } + _, err = db.DeleteByBean(ctx, sw) + return err +} + +// CreateIssueStopwatch creates a stopwatch if not exist, otherwise return an error +func CreateIssueStopwatch(ctx context.Context, user *user_model.User, issue *Issue) error { + if err := issue.LoadRepo(ctx); err != nil { + return err + } + + // if another stopwatch is running: stop it + exists, sw, err := HasUserStopwatch(ctx, user.ID) + if err != nil { + return err + } + if exists { + issue, err := GetIssueByID(ctx, sw.IssueID) + if err != nil { + return err + } + + if err := FinishIssueStopwatch(ctx, user, issue); err != nil { + return err + } + } + + // Create stopwatch + sw = &Stopwatch{ + UserID: user.ID, + IssueID: issue.ID, + } + + if err := db.Insert(ctx, sw); err != nil { + return err + } + + if err := issue.LoadRepo(ctx); err != nil { + return err + } + + if _, err := CreateCommentCtx(ctx, &CreateCommentOptions{ + Doer: user, + Issue: issue, + Repo: issue.Repo, + Type: CommentTypeStartTracking, + }); err != nil { + return err + } + + return nil +} + +// CancelStopwatch removes the given stopwatch and logs it into issue's timeline. +func CancelStopwatch(user *user_model.User, issue *Issue) error { + ctx, committer, err := db.TxContext() + if err != nil { + return err + } + defer committer.Close() + if err := cancelStopwatch(ctx, user, issue); err != nil { + return err + } + return committer.Commit() +} + +func cancelStopwatch(ctx context.Context, user *user_model.User, issue *Issue) error { + e := db.GetEngine(ctx) + sw, exists, err := getStopwatch(ctx, user.ID, issue.ID) + if err != nil { + return err + } + + if exists { + if _, err := e.Delete(sw); err != nil { + return err + } + + if err := issue.LoadRepo(ctx); err != nil { + return err + } + + if _, err := CreateCommentCtx(ctx, &CreateCommentOptions{ + Doer: user, + Issue: issue, + Repo: issue.Repo, + Type: CommentTypeCancelTracking, + }); err != nil { + return err + } + } + return nil +} 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/issues/tracked_time.go b/models/issues/tracked_time.go new file mode 100644 index 0000000000..54179bd3ab --- /dev/null +++ b/models/issues/tracked_time.go @@ -0,0 +1,316 @@ +// 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 ( + "context" + "time" + + "code.gitea.io/gitea/models/db" + user_model "code.gitea.io/gitea/models/user" + "code.gitea.io/gitea/modules/setting" + "code.gitea.io/gitea/modules/util" + + "xorm.io/builder" +) + +// TrackedTime represents a time that was spent for a specific issue. +type TrackedTime struct { + ID int64 `xorm:"pk autoincr"` + IssueID int64 `xorm:"INDEX"` + Issue *Issue `xorm:"-"` + UserID int64 `xorm:"INDEX"` + User *user_model.User `xorm:"-"` + Created time.Time `xorm:"-"` + CreatedUnix int64 `xorm:"created"` + Time int64 `xorm:"NOT NULL"` + Deleted bool `xorm:"NOT NULL DEFAULT false"` +} + +func init() { + db.RegisterModel(new(TrackedTime)) +} + +// TrackedTimeList is a List of TrackedTime's +type TrackedTimeList []*TrackedTime + +// AfterLoad is invoked from XORM after setting the values of all fields of this object. +func (t *TrackedTime) AfterLoad() { + t.Created = time.Unix(t.CreatedUnix, 0).In(setting.DefaultUILocation) +} + +// LoadAttributes load Issue, User +func (t *TrackedTime) LoadAttributes() (err error) { + return t.loadAttributes(db.DefaultContext) +} + +func (t *TrackedTime) loadAttributes(ctx context.Context) (err error) { + if t.Issue == nil { + t.Issue, err = GetIssueByID(ctx, t.IssueID) + if err != nil { + return + } + err = t.Issue.LoadRepo(ctx) + if err != nil { + return + } + } + if t.User == nil { + t.User, err = user_model.GetUserByIDCtx(ctx, t.UserID) + if err != nil { + return + } + } + return +} + +// LoadAttributes load Issue, User +func (tl TrackedTimeList) LoadAttributes() (err error) { + for _, t := range tl { + if err = t.LoadAttributes(); err != nil { + return err + } + } + return +} + +// FindTrackedTimesOptions represent the filters for tracked times. If an ID is 0 it will be ignored. +type FindTrackedTimesOptions struct { + db.ListOptions + IssueID int64 + UserID int64 + RepositoryID int64 + MilestoneID int64 + CreatedAfterUnix int64 + CreatedBeforeUnix int64 +} + +// toCond will convert each condition into a xorm-Cond +func (opts *FindTrackedTimesOptions) toCond() builder.Cond { + cond := builder.NewCond().And(builder.Eq{"tracked_time.deleted": false}) + if opts.IssueID != 0 { + cond = cond.And(builder.Eq{"issue_id": opts.IssueID}) + } + if opts.UserID != 0 { + cond = cond.And(builder.Eq{"user_id": opts.UserID}) + } + if opts.RepositoryID != 0 { + cond = cond.And(builder.Eq{"issue.repo_id": opts.RepositoryID}) + } + if opts.MilestoneID != 0 { + cond = cond.And(builder.Eq{"issue.milestone_id": opts.MilestoneID}) + } + if opts.CreatedAfterUnix != 0 { + cond = cond.And(builder.Gte{"tracked_time.created_unix": opts.CreatedAfterUnix}) + } + if opts.CreatedBeforeUnix != 0 { + cond = cond.And(builder.Lte{"tracked_time.created_unix": opts.CreatedBeforeUnix}) + } + return cond +} + +// toSession will convert the given options to a xorm Session by using the conditions from toCond and joining with issue table if required +func (opts *FindTrackedTimesOptions) toSession(e db.Engine) db.Engine { + sess := e + if opts.RepositoryID > 0 || opts.MilestoneID > 0 { + sess = e.Join("INNER", "issue", "issue.id = tracked_time.issue_id") + } + + sess = sess.Where(opts.toCond()) + + if opts.Page != 0 { + sess = db.SetEnginePagination(sess, opts) + } + + return sess +} + +// GetTrackedTimes returns all tracked times that fit to the given options. +func GetTrackedTimes(ctx context.Context, options *FindTrackedTimesOptions) (trackedTimes TrackedTimeList, err error) { + err = options.toSession(db.GetEngine(ctx)).Find(&trackedTimes) + return +} + +// CountTrackedTimes returns count of tracked times that fit to the given options. +func CountTrackedTimes(opts *FindTrackedTimesOptions) (int64, error) { + sess := db.GetEngine(db.DefaultContext).Where(opts.toCond()) + if opts.RepositoryID > 0 || opts.MilestoneID > 0 { + sess = sess.Join("INNER", "issue", "issue.id = tracked_time.issue_id") + } + return sess.Count(&TrackedTime{}) +} + +// GetTrackedSeconds return sum of seconds +func GetTrackedSeconds(ctx context.Context, opts FindTrackedTimesOptions) (trackedSeconds int64, err error) { + return opts.toSession(db.GetEngine(ctx)).SumInt(&TrackedTime{}, "time") +} + +// AddTime will add the given time (in seconds) to the issue +func AddTime(user *user_model.User, issue *Issue, amount int64, created time.Time) (*TrackedTime, error) { + ctx, committer, err := db.TxContext() + if err != nil { + return nil, err + } + defer committer.Close() + + t, err := addTime(ctx, user, issue, amount, created) + if err != nil { + return nil, err + } + + if err := issue.LoadRepo(ctx); err != nil { + return nil, err + } + + if _, err := CreateCommentCtx(ctx, &CreateCommentOptions{ + Issue: issue, + Repo: issue.Repo, + Doer: user, + Content: util.SecToTime(amount), + Type: CommentTypeAddTimeManual, + TimeID: t.ID, + }); err != nil { + return nil, err + } + + return t, committer.Commit() +} + +func addTime(ctx context.Context, user *user_model.User, issue *Issue, amount int64, created time.Time) (*TrackedTime, error) { + if created.IsZero() { + created = time.Now() + } + tt := &TrackedTime{ + IssueID: issue.ID, + UserID: user.ID, + Time: amount, + Created: created, + } + return tt, db.Insert(ctx, tt) +} + +// TotalTimes returns the spent time for each user by an issue +func TotalTimes(options *FindTrackedTimesOptions) (map[*user_model.User]string, error) { + trackedTimes, err := GetTrackedTimes(db.DefaultContext, options) + if err != nil { + return nil, err + } + // Adding total time per user ID + totalTimesByUser := make(map[int64]int64) + for _, t := range trackedTimes { + totalTimesByUser[t.UserID] += t.Time + } + + totalTimes := make(map[*user_model.User]string) + // Fetching User and making time human readable + for userID, total := range totalTimesByUser { + user, err := user_model.GetUserByID(userID) + if err != nil { + if user_model.IsErrUserNotExist(err) { + continue + } + return nil, err + } + totalTimes[user] = util.SecToTime(total) + } + return totalTimes, nil +} + +// DeleteIssueUserTimes deletes times for issue +func DeleteIssueUserTimes(issue *Issue, user *user_model.User) error { + ctx, committer, err := db.TxContext() + if err != nil { + return err + } + defer committer.Close() + + opts := FindTrackedTimesOptions{ + IssueID: issue.ID, + UserID: user.ID, + } + + removedTime, err := deleteTimes(ctx, opts) + if err != nil { + return err + } + if removedTime == 0 { + return db.ErrNotExist{} + } + + if err := issue.LoadRepo(ctx); err != nil { + return err + } + if _, err := CreateCommentCtx(ctx, &CreateCommentOptions{ + Issue: issue, + Repo: issue.Repo, + Doer: user, + Content: "- " + util.SecToTime(removedTime), + Type: CommentTypeDeleteTimeManual, + }); err != nil { + return err + } + + return committer.Commit() +} + +// DeleteTime delete a specific Time +func DeleteTime(t *TrackedTime) error { + ctx, committer, err := db.TxContext() + if err != nil { + return err + } + defer committer.Close() + + if err := t.loadAttributes(ctx); err != nil { + return err + } + + if err := deleteTime(ctx, t); err != nil { + return err + } + + if _, err := CreateCommentCtx(ctx, &CreateCommentOptions{ + Issue: t.Issue, + Repo: t.Issue.Repo, + Doer: t.User, + Content: "- " + util.SecToTime(t.Time), + Type: CommentTypeDeleteTimeManual, + }); err != nil { + return err + } + + return committer.Commit() +} + +func deleteTimes(ctx context.Context, opts FindTrackedTimesOptions) (removedTime int64, err error) { + removedTime, err = GetTrackedSeconds(ctx, opts) + if err != nil || removedTime == 0 { + return + } + + _, err = opts.toSession(db.GetEngine(ctx)).Table("tracked_time").Cols("deleted").Update(&TrackedTime{Deleted: true}) + return +} + +func deleteTime(ctx context.Context, t *TrackedTime) error { + if t.Deleted { + return db.ErrNotExist{ID: t.ID} + } + t.Deleted = true + _, err := db.GetEngine(ctx).ID(t.ID).Cols("deleted").Update(t) + return err +} + +// GetTrackedTimeByID returns raw TrackedTime without loading attributes by id +func GetTrackedTimeByID(id int64) (*TrackedTime, error) { + time := new(TrackedTime) + has, err := db.GetEngine(db.DefaultContext).ID(id).Get(time) + if err != nil { + return nil, err + } else if !has { + return nil, db.ErrNotExist{ID: id} + } + return time, nil +} diff --git a/models/issues/tracked_time_test.go b/models/issues/tracked_time_test.go new file mode 100644 index 0000000000..787ba9b701 --- /dev/null +++ b/models/issues/tracked_time_test.go @@ -0,0 +1,118 @@ +// Copyright 2019 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" + "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" + + "github.com/stretchr/testify/assert" +) + +func TestAddTime(t *testing.T) { + assert.NoError(t, unittest.PrepareTestDatabase()) + + user3, err := user_model.GetUserByID(3) + assert.NoError(t, err) + + issue1, err := issues_model.GetIssueByID(db.DefaultContext, 1) + assert.NoError(t, err) + + // 3661 = 1h 1min 1s + 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, &issues_model.TrackedTime{UserID: 3, IssueID: 1}).(*issues_model.TrackedTime) + assert.Equal(t, int64(3661), tt.Time) + + 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") +} + +func TestGetTrackedTimes(t *testing.T) { + assert.NoError(t, unittest.PrepareTestDatabase()) + + // by Issue + 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 = issues_model.GetTrackedTimes(db.DefaultContext, &issues_model.FindTrackedTimesOptions{IssueID: -1}) + assert.NoError(t, err) + assert.Len(t, times, 0) + + // by User + 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 = issues_model.GetTrackedTimes(db.DefaultContext, &issues_model.FindTrackedTimesOptions{UserID: 3}) + assert.NoError(t, err) + assert.Len(t, times, 0) + + // by Repo + 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 := issues_model.GetIssueByID(db.DefaultContext, times[0].IssueID) + assert.NoError(t, err) + assert.Equal(t, issue.RepoID, int64(2)) + + times, err = issues_model.GetTrackedTimes(db.DefaultContext, &issues_model.FindTrackedTimesOptions{RepositoryID: 1}) + assert.NoError(t, err) + assert.Len(t, times, 5) + + times, err = issues_model.GetTrackedTimes(db.DefaultContext, &issues_model.FindTrackedTimesOptions{RepositoryID: 10}) + assert.NoError(t, err) + assert.Len(t, times, 0) +} + +func TestTotalTimes(t *testing.T) { + assert.NoError(t, unittest.PrepareTestDatabase()) + + total, err := issues_model.TotalTimes(&issues_model.FindTrackedTimesOptions{IssueID: 1}) + assert.NoError(t, err) + assert.Len(t, total, 1) + for user, time := range total { + assert.Equal(t, int64(1), user.ID) + assert.Equal(t, "6 minutes 40 seconds", time) + } + + total, err = issues_model.TotalTimes(&issues_model.FindTrackedTimesOptions{IssueID: 2}) + assert.NoError(t, err) + assert.Len(t, total, 2) + for user, time := range total { + if user.ID == 2 { + assert.Equal(t, "1 hour 1 minute", time) + } else if user.ID == 1 { + assert.Equal(t, "20 seconds", time) + } else { + assert.Error(t, assert.AnError) + } + } + + total, err = issues_model.TotalTimes(&issues_model.FindTrackedTimesOptions{IssueID: 5}) + assert.NoError(t, err) + assert.Len(t, total, 1) + for user, time := range total { + assert.Equal(t, int64(2), user.ID) + assert.Equal(t, "1 second", time) + } + + 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/pull.go b/models/pull.go deleted file mode 100644 index 238eb16636..0000000000 --- a/models/pull.go +++ /dev/null @@ -1,693 +0,0 @@ -// Copyright 2015 The Gogs Authors. All rights reserved. -// Copyright 2019 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" - "fmt" - "io" - "strings" - - "code.gitea.io/gitea/models/db" - git_model "code.gitea.io/gitea/models/git" - pull_model "code.gitea.io/gitea/models/pull" - repo_model "code.gitea.io/gitea/models/repo" - user_model "code.gitea.io/gitea/models/user" - "code.gitea.io/gitea/modules/git" - "code.gitea.io/gitea/modules/log" - "code.gitea.io/gitea/modules/setting" - "code.gitea.io/gitea/modules/timeutil" - "code.gitea.io/gitea/modules/util" - - "xorm.io/builder" -) - -// PullRequestType defines pull request type -type PullRequestType int - -// Enumerate all the pull request types -const ( - PullRequestGitea PullRequestType = iota - PullRequestGit -) - -// PullRequestStatus defines pull request status -type PullRequestStatus int - -// Enumerate all the pull request status -const ( - PullRequestStatusConflict PullRequestStatus = iota - PullRequestStatusChecking - PullRequestStatusMergeable - PullRequestStatusManuallyMerged - PullRequestStatusError - PullRequestStatusEmpty -) - -// PullRequestFlow the flow of pull request -type PullRequestFlow int - -const ( - // PullRequestFlowGithub github flow from head branch to base branch - PullRequestFlowGithub PullRequestFlow = iota - // PullRequestFlowAGit Agit flow pull request, head branch is not exist - PullRequestFlowAGit -) - -// PullRequest represents relation between pull request and repositories. -type PullRequest struct { - ID int64 `xorm:"pk autoincr"` - Type PullRequestType - Status PullRequestStatus - ConflictedFiles []string `xorm:"TEXT JSON"` - CommitsAhead int - CommitsBehind int - - ChangedProtectedFiles []string `xorm:"TEXT JSON"` - - IssueID int64 `xorm:"INDEX"` - Issue *Issue `xorm:"-"` - Index int64 - - HeadRepoID int64 `xorm:"INDEX"` - HeadRepo *repo_model.Repository `xorm:"-"` - BaseRepoID int64 `xorm:"INDEX"` - BaseRepo *repo_model.Repository `xorm:"-"` - HeadBranch string - HeadCommitID string `xorm:"-"` - BaseBranch string - ProtectedBranch *git_model.ProtectedBranch `xorm:"-"` - MergeBase string `xorm:"VARCHAR(40)"` - AllowMaintainerEdit bool `xorm:"NOT NULL DEFAULT false"` - - HasMerged bool `xorm:"INDEX"` - MergedCommitID string `xorm:"VARCHAR(40)"` - MergerID int64 `xorm:"INDEX"` - Merger *user_model.User `xorm:"-"` - MergedUnix timeutil.TimeStamp `xorm:"updated INDEX"` - - isHeadRepoLoaded bool `xorm:"-"` - - Flow PullRequestFlow `xorm:"NOT NULL DEFAULT 0"` -} - -func init() { - db.RegisterModel(new(PullRequest)) -} - -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 - if _, err := db.GetEngine(ctx).In("pull_id", deleteCond). - Delete(&pull_model.AutoMerge{}); err != nil { - return err - } - - // Delete review states - if _, err := db.GetEngine(ctx).In("pull_id", deleteCond). - Delete(&pull_model.ReviewState{}); err != nil { - return err - } - - _, err := db.DeleteByBean(ctx, &PullRequest{BaseRepoID: repoID}) - return err -} - -// MustHeadUserName returns the HeadRepo's username if failed return blank -func (pr *PullRequest) MustHeadUserName() string { - if err := pr.LoadHeadRepo(); err != nil { - if !repo_model.IsErrRepoNotExist(err) { - log.Error("LoadHeadRepo: %v", err) - } else { - log.Warn("LoadHeadRepo %d but repository does not exist: %v", pr.HeadRepoID, err) - } - return "" - } - if pr.HeadRepo == nil { - return "" - } - return pr.HeadRepo.OwnerName -} - -// Note: don't try to get Issue because will end up recursive querying. -func (pr *PullRequest) loadAttributes(ctx context.Context) (err error) { - if pr.HasMerged && pr.Merger == nil { - pr.Merger, err = user_model.GetUserByIDCtx(ctx, pr.MergerID) - if user_model.IsErrUserNotExist(err) { - pr.MergerID = -1 - pr.Merger = user_model.NewGhostUser() - } else if err != nil { - return fmt.Errorf("getUserByID [%d]: %v", pr.MergerID, err) - } - } - - return nil -} - -// LoadAttributes loads pull request attributes from database -func (pr *PullRequest) LoadAttributes() error { - return pr.loadAttributes(db.DefaultContext) -} - -// LoadHeadRepoCtx loads the head repository -func (pr *PullRequest) LoadHeadRepoCtx(ctx context.Context) (err error) { - if !pr.isHeadRepoLoaded && pr.HeadRepo == nil && pr.HeadRepoID > 0 { - if pr.HeadRepoID == pr.BaseRepoID { - if pr.BaseRepo != nil { - pr.HeadRepo = pr.BaseRepo - return nil - } else if pr.Issue != nil && pr.Issue.Repo != nil { - pr.HeadRepo = pr.Issue.Repo - return nil - } - } - - pr.HeadRepo, err = repo_model.GetRepositoryByIDCtx(ctx, pr.HeadRepoID) - if err != nil && !repo_model.IsErrRepoNotExist(err) { // Head repo maybe deleted, but it should still work - return fmt.Errorf("getRepositoryByID(head): %v", err) - } - pr.isHeadRepoLoaded = true - } - return nil -} - -// LoadHeadRepo loads the head repository -func (pr *PullRequest) LoadHeadRepo() error { - return pr.LoadHeadRepoCtx(db.DefaultContext) -} - -// LoadBaseRepo loads the target repository -func (pr *PullRequest) LoadBaseRepo() error { - return pr.LoadBaseRepoCtx(db.DefaultContext) -} - -// LoadBaseRepoCtx loads the target repository -func (pr *PullRequest) LoadBaseRepoCtx(ctx context.Context) (err error) { - if pr.BaseRepo != nil { - return nil - } - - if pr.HeadRepoID == pr.BaseRepoID && pr.HeadRepo != nil { - pr.BaseRepo = pr.HeadRepo - return nil - } - - if pr.Issue != nil && pr.Issue.Repo != nil { - pr.BaseRepo = pr.Issue.Repo - return nil - } - - pr.BaseRepo, err = repo_model.GetRepositoryByIDCtx(ctx, pr.BaseRepoID) - if err != nil { - return fmt.Errorf("repo_model.GetRepositoryByID(base): %v", err) - } - return nil -} - -// LoadIssue loads issue information from database -func (pr *PullRequest) LoadIssue() (err error) { - return pr.LoadIssueCtx(db.DefaultContext) -} - -// LoadIssueCtx loads issue information from database -func (pr *PullRequest) LoadIssueCtx(ctx context.Context) (err error) { - if pr.Issue != nil { - return nil - } - - pr.Issue, err = getIssueByID(ctx, pr.IssueID) - if err == nil { - pr.Issue.PullRequest = pr - } - return err -} - -// LoadProtectedBranch loads the protected branch of the base branch -func (pr *PullRequest) LoadProtectedBranch() (err error) { - return pr.LoadProtectedBranchCtx(db.DefaultContext) -} - -// LoadProtectedBranchCtx loads the protected branch of the base branch -func (pr *PullRequest) LoadProtectedBranchCtx(ctx context.Context) (err error) { - if pr.ProtectedBranch == nil { - if pr.BaseRepo == nil { - if pr.BaseRepoID == 0 { - return nil - } - pr.BaseRepo, err = repo_model.GetRepositoryByIDCtx(ctx, pr.BaseRepoID) - if err != nil { - return - } - } - pr.ProtectedBranch, err = git_model.GetProtectedBranchBy(ctx, pr.BaseRepo.ID, pr.BaseBranch) - } - return -} - -// ReviewCount represents a count of Reviews -type ReviewCount struct { - IssueID int64 - Type ReviewType - Count int64 -} - -// GetApprovalCounts returns the approval counts by type -// FIXME: Only returns official counts due to double counting of non-official counts -func (pr *PullRequest) GetApprovalCounts(ctx context.Context) ([]*ReviewCount, error) { - rCounts := make([]*ReviewCount, 0, 6) - sess := db.GetEngine(ctx).Where("issue_id = ?", pr.IssueID) - return rCounts, sess.Select("issue_id, type, count(id) as `count`").Where("official = ? AND dismissed = ?", true, false).GroupBy("issue_id, type").Table("review").Find(&rCounts) -} - -// GetApprovers returns the approvers of the pull request -func (pr *PullRequest) GetApprovers() string { - stringBuilder := strings.Builder{} - if err := pr.getReviewedByLines(&stringBuilder); err != nil { - log.Error("Unable to getReviewedByLines: Error: %v", err) - return "" - } - - return stringBuilder.String() -} - -func (pr *PullRequest) getReviewedByLines(writer io.Writer) error { - maxReviewers := setting.Repository.PullRequest.DefaultMergeMessageMaxApprovers - - if maxReviewers == 0 { - return nil - } - - ctx, committer, err := db.TxContext() - if err != nil { - return err - } - defer committer.Close() - - // Note: This doesn't page as we only expect a very limited number of reviews - reviews, err := FindReviews(ctx, FindReviewOptions{ - Type: ReviewTypeApprove, - IssueID: pr.IssueID, - OfficialOnly: setting.Repository.PullRequest.DefaultMergeMessageOfficialApproversOnly, - }) - if err != nil { - log.Error("Unable to FindReviews for PR ID %d: %v", pr.ID, err) - return err - } - - reviewersWritten := 0 - - for _, review := range reviews { - if maxReviewers > 0 && reviewersWritten > maxReviewers { - break - } - - if err := review.loadReviewer(ctx); err != nil && !user_model.IsErrUserNotExist(err) { - log.Error("Unable to LoadReviewer[%d] for PR ID %d : %v", review.ReviewerID, pr.ID, err) - return err - } else if review.Reviewer == nil { - continue - } - if _, err := writer.Write([]byte("Reviewed-by: ")); err != nil { - return err - } - if _, err := writer.Write([]byte(review.Reviewer.NewGitSig().String())); err != nil { - return err - } - if _, err := writer.Write([]byte{'\n'}); err != nil { - return err - } - reviewersWritten++ - } - return committer.Commit() -} - -// GetGitRefName returns git ref for hidden pull request branch -func (pr *PullRequest) GetGitRefName() string { - return fmt.Sprintf("%s%d/head", git.PullPrefix, pr.Index) -} - -// IsChecking returns true if this pull request is still checking conflict. -func (pr *PullRequest) IsChecking() bool { - return pr.Status == PullRequestStatusChecking -} - -// CanAutoMerge returns true if this pull request can be merged automatically. -func (pr *PullRequest) CanAutoMerge() bool { - return pr.Status == PullRequestStatusMergeable -} - -// IsEmpty returns true if this pull request is empty. -func (pr *PullRequest) IsEmpty() bool { - return pr.Status == PullRequestStatusEmpty -} - -// SetMerged sets a pull request to merged and closes the corresponding issue -func (pr *PullRequest) SetMerged(ctx context.Context) (bool, error) { - if pr.HasMerged { - return false, fmt.Errorf("PullRequest[%d] already merged", pr.Index) - } - if pr.MergedCommitID == "" || pr.MergedUnix == 0 || pr.Merger == nil { - return false, fmt.Errorf("Unable to merge PullRequest[%d], some required fields are empty", pr.Index) - } - - pr.HasMerged = true - sess := db.GetEngine(ctx) - - if _, err := sess.Exec("UPDATE `issue` SET `repo_id` = `repo_id` WHERE `id` = ?", pr.IssueID); err != nil { - return false, err - } - - if _, err := sess.Exec("UPDATE `pull_request` SET `issue_id` = `issue_id` WHERE `id` = ?", pr.ID); err != nil { - return false, err - } - - pr.Issue = nil - if err := pr.LoadIssueCtx(ctx); err != nil { - return false, err - } - - if tmpPr, err := GetPullRequestByID(ctx, pr.ID); err != nil { - return false, err - } else if tmpPr.HasMerged { - if pr.Issue.IsClosed { - return false, nil - } - return false, fmt.Errorf("PullRequest[%d] already merged but it's associated issue [%d] is not closed", pr.Index, pr.IssueID) - } else if pr.Issue.IsClosed { - return false, fmt.Errorf("PullRequest[%d] already closed", pr.Index) - } - - if err := pr.Issue.LoadRepo(ctx); err != nil { - return false, err - } - - if err := pr.Issue.Repo.GetOwner(ctx); err != nil { - return false, err - } - - if _, err := changeIssueStatus(ctx, pr.Issue, pr.Merger, true, true); err != nil { - return false, fmt.Errorf("Issue.changeStatus: %v", err) - } - - // reset the conflicted files as there cannot be any if we're merged - pr.ConflictedFiles = []string{} - - // We need to save all of the data used to compute this merge as it may have already been changed by TestPatch. FIXME: need to set some state to prevent TestPatch from running whilst we are merging. - if _, err := sess.Where("id = ?", pr.ID).Cols("has_merged, status, merge_base, merged_commit_id, merger_id, merged_unix, conflicted_files").Update(pr); err != nil { - return false, fmt.Errorf("Failed to update pr[%d]: %v", pr.ID, err) - } - - return true, nil -} - -// NewPullRequest creates new pull request with labels for repository. -func NewPullRequest(outerCtx context.Context, repo *repo_model.Repository, issue *Issue, labelIDs []int64, uuids []string, pr *PullRequest) (err error) { - idx, err := db.GetNextResourceIndex("issue_index", repo.ID) - if err != nil { - return fmt.Errorf("generate pull request index failed: %v", err) - } - - issue.Index = idx - - ctx, committer, err := db.TxContext() - if err != nil { - return err - } - defer committer.Close() - ctx.WithContext(outerCtx) - - if err = newIssue(ctx, issue.Poster, NewIssueOptions{ - Repo: repo, - Issue: issue, - LabelIDs: labelIDs, - Attachments: uuids, - IsPull: true, - }); err != nil { - if IsErrUserDoesNotHaveAccessToRepo(err) || IsErrNewIssueInsert(err) { - return err - } - return fmt.Errorf("newIssue: %v", err) - } - - pr.Index = issue.Index - pr.BaseRepo = repo - pr.IssueID = issue.ID - if err = db.Insert(ctx, pr); err != nil { - return fmt.Errorf("insert pull repo: %v", err) - } - - if err = committer.Commit(); err != nil { - return fmt.Errorf("Commit: %v", err) - } - - return nil -} - -// GetUnmergedPullRequest returns a pull request that is open and has not been merged -// by given head/base and repo/branch. -func GetUnmergedPullRequest(headRepoID, baseRepoID int64, headBranch, baseBranch string, flow PullRequestFlow) (*PullRequest, error) { - pr := new(PullRequest) - has, err := db.GetEngine(db.DefaultContext). - Where("head_repo_id=? AND head_branch=? AND base_repo_id=? AND base_branch=? AND has_merged=? AND flow = ? AND issue.is_closed=?", - headRepoID, headBranch, baseRepoID, baseBranch, false, flow, false). - Join("INNER", "issue", "issue.id=pull_request.issue_id"). - Get(pr) - if err != nil { - return nil, err - } else if !has { - return nil, ErrPullRequestNotExist{0, 0, headRepoID, baseRepoID, headBranch, baseBranch} - } - - return pr, nil -} - -// GetLatestPullRequestByHeadInfo returns the latest pull request (regardless of its status) -// by given head information (repo and branch). -func GetLatestPullRequestByHeadInfo(repoID int64, branch string) (*PullRequest, error) { - pr := new(PullRequest) - has, err := db.GetEngine(db.DefaultContext). - Where("head_repo_id = ? AND head_branch = ? AND flow = ?", repoID, branch, PullRequestFlowGithub). - OrderBy("id DESC"). - Get(pr) - if !has { - return nil, err - } - return pr, err -} - -// GetPullRequestByIndex returns a pull request by the given index -func GetPullRequestByIndex(ctx context.Context, repoID, index int64) (*PullRequest, error) { - if index < 1 { - return nil, ErrPullRequestNotExist{} - } - pr := &PullRequest{ - BaseRepoID: repoID, - Index: index, - } - - has, err := db.GetEngine(ctx).Get(pr) - if err != nil { - return nil, err - } else if !has { - return nil, ErrPullRequestNotExist{0, 0, 0, repoID, "", ""} - } - - if err = pr.loadAttributes(ctx); err != nil { - return nil, err - } - if err = pr.LoadIssueCtx(ctx); err != nil { - return nil, err - } - - return pr, nil -} - -// GetPullRequestByID returns a pull request by given ID. -func GetPullRequestByID(ctx context.Context, id int64) (*PullRequest, error) { - pr := new(PullRequest) - has, err := db.GetEngine(ctx).ID(id).Get(pr) - if err != nil { - return nil, err - } else if !has { - return nil, ErrPullRequestNotExist{id, 0, 0, 0, "", ""} - } - return pr, pr.loadAttributes(ctx) -} - -// GetPullRequestByIssueIDWithNoAttributes returns pull request with no attributes loaded by given issue ID. -func GetPullRequestByIssueIDWithNoAttributes(issueID int64) (*PullRequest, error) { - var pr PullRequest - has, err := db.GetEngine(db.DefaultContext).Where("issue_id = ?", issueID).Get(&pr) - if err != nil { - return nil, err - } - if !has { - return nil, ErrPullRequestNotExist{0, issueID, 0, 0, "", ""} - } - return &pr, nil -} - -// GetPullRequestByIssueID returns pull request by given issue ID. -func GetPullRequestByIssueID(ctx context.Context, issueID int64) (*PullRequest, error) { - pr := &PullRequest{ - IssueID: issueID, - } - has, err := db.GetByBean(ctx, pr) - if err != nil { - return nil, err - } else if !has { - return nil, ErrPullRequestNotExist{0, issueID, 0, 0, "", ""} - } - return pr, pr.loadAttributes(ctx) -} - -// GetAllUnmergedAgitPullRequestByPoster get all unmerged agit flow pull request -// By poster id. -func GetAllUnmergedAgitPullRequestByPoster(uid int64) ([]*PullRequest, error) { - pulls := make([]*PullRequest, 0, 10) - - err := db.GetEngine(db.DefaultContext). - Where("has_merged=? AND flow = ? AND issue.is_closed=? AND issue.poster_id=?", - false, PullRequestFlowAGit, false, uid). - Join("INNER", "issue", "issue.id=pull_request.issue_id"). - Find(&pulls) - - return pulls, err -} - -// Update updates all fields of pull request. -func (pr *PullRequest) Update() error { - _, err := db.GetEngine(db.DefaultContext).ID(pr.ID).AllCols().Update(pr) - return err -} - -// UpdateCols updates specific fields of pull request. -func (pr *PullRequest) UpdateCols(cols ...string) error { - _, err := db.GetEngine(db.DefaultContext).ID(pr.ID).Cols(cols...).Update(pr) - return err -} - -// UpdateColsIfNotMerged updates specific fields of a pull request if it has not been merged -func (pr *PullRequest) UpdateColsIfNotMerged(cols ...string) error { - _, err := db.GetEngine(db.DefaultContext).Where("id = ? AND has_merged = ?", pr.ID, false).Cols(cols...).Update(pr) - return err -} - -// IsWorkInProgress determine if the Pull Request is a Work In Progress by its title -func (pr *PullRequest) IsWorkInProgress() bool { - if err := pr.LoadIssue(); err != nil { - log.Error("LoadIssue: %v", err) - return false - } - return HasWorkInProgressPrefix(pr.Issue.Title) -} - -// HasWorkInProgressPrefix determines if the given PR title has a Work In Progress prefix -func HasWorkInProgressPrefix(title string) bool { - for _, prefix := range setting.Repository.PullRequest.WorkInProgressPrefixes { - if strings.HasPrefix(strings.ToUpper(title), strings.ToUpper(prefix)) { - return true - } - } - return false -} - -// IsFilesConflicted determines if the Pull Request has changes conflicting with the target branch. -func (pr *PullRequest) IsFilesConflicted() bool { - return len(pr.ConflictedFiles) > 0 -} - -// GetWorkInProgressPrefix returns the prefix used to mark the pull request as a work in progress. -// It returns an empty string when none were found -func (pr *PullRequest) GetWorkInProgressPrefix() string { - if err := pr.LoadIssue(); err != nil { - log.Error("LoadIssue: %v", err) - return "" - } - - for _, prefix := range setting.Repository.PullRequest.WorkInProgressPrefixes { - if strings.HasPrefix(strings.ToUpper(pr.Issue.Title), strings.ToUpper(prefix)) { - return pr.Issue.Title[0:len(prefix)] - } - } - return "" -} - -// UpdateCommitDivergence update Divergence of a pull request -func (pr *PullRequest) UpdateCommitDivergence(ctx context.Context, ahead, behind int) error { - if pr.ID == 0 { - return fmt.Errorf("pull ID is 0") - } - pr.CommitsAhead = ahead - pr.CommitsBehind = behind - _, err := db.GetEngine(ctx).ID(pr.ID).Cols("commits_ahead", "commits_behind").Update(pr) - return err -} - -// IsSameRepo returns true if base repo and head repo is the same -func (pr *PullRequest) IsSameRepo() bool { - return pr.BaseRepoID == pr.HeadRepoID -} - -// GetPullRequestsByHeadBranch returns all prs by head branch -// Since there could be multiple prs with the same head branch, this function returns a slice of prs -func GetPullRequestsByHeadBranch(ctx context.Context, headBranch string, headRepoID int64) ([]*PullRequest, error) { - log.Trace("GetPullRequestsByHeadBranch: headBranch: '%s', headRepoID: '%d'", headBranch, headRepoID) - prs := make([]*PullRequest, 0, 2) - if err := db.GetEngine(ctx).Where(builder.Eq{"head_branch": headBranch, "head_repo_id": headRepoID}). - Find(&prs); err != nil { - return nil, err - } - return prs, nil -} - -// GetBaseBranchHTMLURL returns the HTML URL of the base branch -func (pr *PullRequest) GetBaseBranchHTMLURL() string { - if err := pr.LoadBaseRepo(); err != nil { - log.Error("LoadBaseRepo: %v", err) - return "" - } - if pr.BaseRepo == nil { - return "" - } - return pr.BaseRepo.HTMLURL() + "/src/branch/" + util.PathEscapeSegments(pr.BaseBranch) -} - -// GetHeadBranchHTMLURL returns the HTML URL of the head branch -func (pr *PullRequest) GetHeadBranchHTMLURL() string { - if pr.Flow == PullRequestFlowAGit { - return "" - } - - if err := pr.LoadHeadRepo(); err != nil { - log.Error("LoadHeadRepo: %v", err) - return "" - } - if pr.HeadRepo == nil { - return "" - } - return pr.HeadRepo.HTMLURL() + "/src/branch/" + util.PathEscapeSegments(pr.HeadBranch) -} - -// UpdateAllowEdits update if PR can be edited from maintainers -func UpdateAllowEdits(ctx context.Context, pr *PullRequest) error { - if _, err := db.GetEngine(ctx).ID(pr.ID).Cols("allow_maintainer_edit").Update(pr); err != nil { - return err - } - return nil -} - -// Mergeable returns if the pullrequest is mergeable. -func (pr *PullRequest) Mergeable() bool { - // If a pull request isn't mergable if it's: - // - Being conflict checked. - // - Has a conflict. - // - Received a error while being conflict checked. - // - Is a work-in-progress pull request. - return pr.Status != PullRequestStatusChecking && pr.Status != PullRequestStatusConflict && - pr.Status != PullRequestStatusError && !pr.IsWorkInProgress() -} diff --git a/models/pull_list.go b/models/pull_list.go deleted file mode 100644 index fb14d3beac..0000000000 --- a/models/pull_list.go +++ /dev/null @@ -1,216 +0,0 @@ -// Copyright 2019 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" - "fmt" - - "code.gitea.io/gitea/models/db" - access_model "code.gitea.io/gitea/models/perm/access" - "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/git" - "code.gitea.io/gitea/modules/log" - - "xorm.io/xorm" -) - -// PullRequestsOptions holds the options for PRs -type PullRequestsOptions struct { - db.ListOptions - State string - SortType string - Labels []string - MilestoneID int64 -} - -func listPullRequestStatement(baseRepoID int64, opts *PullRequestsOptions) (*xorm.Session, error) { - sess := db.GetEngine(db.DefaultContext).Where("pull_request.base_repo_id=?", baseRepoID) - - sess.Join("INNER", "issue", "pull_request.issue_id = issue.id") - switch opts.State { - case "closed", "open": - sess.And("issue.is_closed=?", opts.State == "closed") - } - - if labelIDs, err := base.StringsToInt64s(opts.Labels); err != nil { - return nil, err - } else if len(labelIDs) > 0 { - sess.Join("INNER", "issue_label", "issue.id = issue_label.issue_id"). - In("issue_label.label_id", labelIDs) - } - - if opts.MilestoneID > 0 { - sess.And("issue.milestone_id=?", opts.MilestoneID) - } - - return sess, nil -} - -// GetUnmergedPullRequestsByHeadInfo returns all pull requests that are open and has not been merged -// by given head information (repo and branch). -func GetUnmergedPullRequestsByHeadInfo(repoID int64, branch string) ([]*PullRequest, error) { - prs := make([]*PullRequest, 0, 2) - return prs, db.GetEngine(db.DefaultContext). - Where("head_repo_id = ? AND head_branch = ? AND has_merged = ? AND issue.is_closed = ? AND flow = ?", - repoID, branch, false, false, PullRequestFlowGithub). - Join("INNER", "issue", "issue.id = pull_request.issue_id"). - Find(&prs) -} - -// CanMaintainerWriteToBranch check whether user is a matainer and could write to the branch -func CanMaintainerWriteToBranch(p access_model.Permission, branch string, user *user_model.User) bool { - if p.CanWrite(unit.TypeCode) { - return true - } - - if len(p.Units) < 1 { - return false - } - - prs, err := GetUnmergedPullRequestsByHeadInfo(p.Units[0].RepoID, branch) - if err != nil { - return false - } - - for _, pr := range prs { - if pr.AllowMaintainerEdit { - err = pr.LoadBaseRepo() - if err != nil { - continue - } - prPerm, err := access_model.GetUserRepoPermission(db.DefaultContext, pr.BaseRepo, user) - if err != nil { - continue - } - if prPerm.CanWrite(unit.TypeCode) { - return true - } - } - } - return false -} - -// HasUnmergedPullRequestsByHeadInfo checks if there are open and not merged pull request -// by given head information (repo and branch) -func HasUnmergedPullRequestsByHeadInfo(ctx context.Context, repoID int64, branch string) (bool, error) { - return db.GetEngine(ctx). - Where("head_repo_id = ? AND head_branch = ? AND has_merged = ? AND issue.is_closed = ? AND flow = ?", - repoID, branch, false, false, PullRequestFlowGithub). - Join("INNER", "issue", "issue.id = pull_request.issue_id"). - Exist(&PullRequest{}) -} - -// GetUnmergedPullRequestsByBaseInfo returns all pull requests that are open and has not been merged -// by given base information (repo and branch). -func GetUnmergedPullRequestsByBaseInfo(repoID int64, branch string) ([]*PullRequest, error) { - prs := make([]*PullRequest, 0, 2) - return prs, db.GetEngine(db.DefaultContext). - Where("base_repo_id=? AND base_branch=? AND has_merged=? AND issue.is_closed=?", - repoID, branch, false, false). - Join("INNER", "issue", "issue.id=pull_request.issue_id"). - Find(&prs) -} - -// GetPullRequestIDsByCheckStatus returns all pull requests according the special checking status. -func GetPullRequestIDsByCheckStatus(status PullRequestStatus) ([]int64, error) { - prs := make([]int64, 0, 10) - return prs, db.GetEngine(db.DefaultContext).Table("pull_request"). - Where("status=?", status). - Cols("pull_request.id"). - Find(&prs) -} - -// PullRequests returns all pull requests for a base Repo by the given conditions -func PullRequests(baseRepoID int64, opts *PullRequestsOptions) ([]*PullRequest, int64, error) { - if opts.Page <= 0 { - opts.Page = 1 - } - - countSession, err := listPullRequestStatement(baseRepoID, opts) - if err != nil { - log.Error("listPullRequestStatement: %v", err) - return nil, 0, err - } - maxResults, err := countSession.Count(new(PullRequest)) - if err != nil { - log.Error("Count PRs: %v", err) - return nil, maxResults, err - } - - findSession, err := listPullRequestStatement(baseRepoID, opts) - sortIssuesSession(findSession, opts.SortType, 0) - if err != nil { - log.Error("listPullRequestStatement: %v", err) - return nil, maxResults, err - } - findSession = db.SetSessionPagination(findSession, opts) - prs := make([]*PullRequest, 0, opts.PageSize) - return prs, maxResults, findSession.Find(&prs) -} - -// PullRequestList defines a list of pull requests -type PullRequestList []*PullRequest - -func (prs PullRequestList) loadAttributes(ctx context.Context) error { - if len(prs) == 0 { - return nil - } - - // Load issues. - issueIDs := prs.getIssueIDs() - issues := make([]*Issue, 0, len(issueIDs)) - if err := db.GetEngine(ctx). - Where("id > 0"). - In("id", issueIDs). - Find(&issues); err != nil { - return fmt.Errorf("find issues: %v", err) - } - - set := make(map[int64]*Issue) - for i := range issues { - set[issues[i].ID] = issues[i] - } - for i := range prs { - prs[i].Issue = set[prs[i].IssueID] - } - return nil -} - -func (prs PullRequestList) getIssueIDs() []int64 { - issueIDs := make([]int64, 0, len(prs)) - for i := range prs { - issueIDs = append(issueIDs, prs[i].IssueID) - } - return issueIDs -} - -// LoadAttributes load all the prs attributes -func (prs PullRequestList) LoadAttributes() error { - return prs.loadAttributes(db.DefaultContext) -} - -// InvalidateCodeComments will lookup the prs for code comments which got invalidated by change -func (prs PullRequestList) InvalidateCodeComments(ctx context.Context, doer *user_model.User, repo *git.Repository, branch string) error { - if len(prs) == 0 { - return nil - } - issueIDs := prs.getIssueIDs() - var codeComments []*Comment - if err := db.GetEngine(ctx). - Where("type = ? and invalidated = ?", CommentTypeCode, false). - In("issue_id", issueIDs). - Find(&codeComments); err != nil { - return fmt.Errorf("find code comments: %v", err) - } - for _, comment := range codeComments { - if err := comment.CheckInvalidation(repo, doer, branch); err != nil { - return err - } - } - return nil -} diff --git a/models/pull_test.go b/models/pull_test.go deleted file mode 100644 index 00bbfc798a..0000000000 --- a/models/pull_test.go +++ /dev/null @@ -1,255 +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 ( - "testing" - - "code.gitea.io/gitea/models/db" - "code.gitea.io/gitea/models/unittest" - - "github.com/stretchr/testify/assert" -) - -func TestPullRequest_LoadAttributes(t *testing.T) { - assert.NoError(t, unittest.PrepareTestDatabase()) - pr := unittest.AssertExistsAndLoadBean(t, &PullRequest{ID: 1}).(*PullRequest) - assert.NoError(t, pr.LoadAttributes()) - assert.NotNil(t, pr.Merger) - assert.Equal(t, pr.MergerID, pr.Merger.ID) -} - -func TestPullRequest_LoadIssue(t *testing.T) { - assert.NoError(t, unittest.PrepareTestDatabase()) - pr := unittest.AssertExistsAndLoadBean(t, &PullRequest{ID: 1}).(*PullRequest) - assert.NoError(t, pr.LoadIssue()) - assert.NotNil(t, pr.Issue) - assert.Equal(t, int64(2), pr.Issue.ID) - assert.NoError(t, pr.LoadIssue()) - assert.NotNil(t, pr.Issue) - assert.Equal(t, int64(2), pr.Issue.ID) -} - -func TestPullRequest_LoadBaseRepo(t *testing.T) { - assert.NoError(t, unittest.PrepareTestDatabase()) - pr := unittest.AssertExistsAndLoadBean(t, &PullRequest{ID: 1}).(*PullRequest) - assert.NoError(t, pr.LoadBaseRepo()) - assert.NotNil(t, pr.BaseRepo) - assert.Equal(t, pr.BaseRepoID, pr.BaseRepo.ID) - assert.NoError(t, pr.LoadBaseRepo()) - assert.NotNil(t, pr.BaseRepo) - assert.Equal(t, pr.BaseRepoID, pr.BaseRepo.ID) -} - -func TestPullRequest_LoadHeadRepo(t *testing.T) { - assert.NoError(t, unittest.PrepareTestDatabase()) - pr := unittest.AssertExistsAndLoadBean(t, &PullRequest{ID: 1}).(*PullRequest) - assert.NoError(t, pr.LoadHeadRepo()) - assert.NotNil(t, pr.HeadRepo) - assert.Equal(t, pr.HeadRepoID, pr.HeadRepo.ID) -} - -// TODO TestMerge - -// TODO TestNewPullRequest - -func TestPullRequestsNewest(t *testing.T) { - assert.NoError(t, unittest.PrepareTestDatabase()) - prs, count, err := PullRequests(1, &PullRequestsOptions{ - ListOptions: db.ListOptions{ - Page: 1, - }, - State: "open", - SortType: "newest", - Labels: []string{}, - }) - assert.NoError(t, err) - assert.EqualValues(t, 3, count) - if assert.Len(t, prs, 3) { - assert.EqualValues(t, 5, prs[0].ID) - assert.EqualValues(t, 2, prs[1].ID) - assert.EqualValues(t, 1, prs[2].ID) - } -} - -func TestPullRequestsOldest(t *testing.T) { - assert.NoError(t, unittest.PrepareTestDatabase()) - prs, count, err := PullRequests(1, &PullRequestsOptions{ - ListOptions: db.ListOptions{ - Page: 1, - }, - State: "open", - SortType: "oldest", - Labels: []string{}, - }) - assert.NoError(t, err) - assert.EqualValues(t, 3, count) - if assert.Len(t, prs, 3) { - assert.EqualValues(t, 1, prs[0].ID) - assert.EqualValues(t, 2, prs[1].ID) - assert.EqualValues(t, 5, prs[2].ID) - } -} - -func TestGetUnmergedPullRequest(t *testing.T) { - assert.NoError(t, unittest.PrepareTestDatabase()) - pr, err := GetUnmergedPullRequest(1, 1, "branch2", "master", PullRequestFlowGithub) - assert.NoError(t, err) - assert.Equal(t, int64(2), pr.ID) - - _, err = GetUnmergedPullRequest(1, 9223372036854775807, "branch1", "master", PullRequestFlowGithub) - assert.Error(t, err) - assert.True(t, IsErrPullRequestNotExist(err)) -} - -func TestHasUnmergedPullRequestsByHeadInfo(t *testing.T) { - assert.NoError(t, unittest.PrepareTestDatabase()) - - exist, err := HasUnmergedPullRequestsByHeadInfo(db.DefaultContext, 1, "branch2") - assert.NoError(t, err) - assert.Equal(t, true, exist) - - exist, err = 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") - assert.NoError(t, err) - assert.Len(t, prs, 1) - for _, pr := range prs { - assert.Equal(t, int64(1), pr.HeadRepoID) - assert.Equal(t, "branch2", pr.HeadBranch) - } -} - -func TestGetUnmergedPullRequestsByBaseInfo(t *testing.T) { - assert.NoError(t, unittest.PrepareTestDatabase()) - prs, err := GetUnmergedPullRequestsByBaseInfo(1, "master") - assert.NoError(t, err) - assert.Len(t, prs, 1) - pr := prs[0] - assert.Equal(t, int64(2), pr.ID) - assert.Equal(t, int64(1), pr.BaseRepoID) - assert.Equal(t, "master", pr.BaseBranch) -} - -func TestGetPullRequestByIndex(t *testing.T) { - assert.NoError(t, unittest.PrepareTestDatabase()) - pr, err := 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) - assert.Error(t, err) - assert.True(t, IsErrPullRequestNotExist(err)) - - _, err = GetPullRequestByIndex(db.DefaultContext, 1, 0) - assert.Error(t, err) - assert.True(t, IsErrPullRequestNotExist(err)) -} - -func TestGetPullRequestByID(t *testing.T) { - assert.NoError(t, unittest.PrepareTestDatabase()) - pr, err := 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) - assert.Error(t, err) - assert.True(t, IsErrPullRequestNotExist(err)) -} - -func TestGetPullRequestByIssueID(t *testing.T) { - assert.NoError(t, unittest.PrepareTestDatabase()) - pr, err := GetPullRequestByIssueID(db.DefaultContext, 2) - assert.NoError(t, err) - assert.Equal(t, int64(2), pr.IssueID) - - _, err = GetPullRequestByIssueID(db.DefaultContext, 9223372036854775807) - assert.Error(t, err) - assert.True(t, IsErrPullRequestNotExist(err)) -} - -func TestPullRequest_Update(t *testing.T) { - assert.NoError(t, unittest.PrepareTestDatabase()) - pr := unittest.AssertExistsAndLoadBean(t, &PullRequest{ID: 1}).(*PullRequest) - pr.BaseBranch = "baseBranch" - pr.HeadBranch = "headBranch" - pr.Update() - - pr = unittest.AssertExistsAndLoadBean(t, &PullRequest{ID: pr.ID}).(*PullRequest) - assert.Equal(t, "baseBranch", pr.BaseBranch) - assert.Equal(t, "headBranch", pr.HeadBranch) - unittest.CheckConsistencyFor(t, pr) -} - -func TestPullRequest_UpdateCols(t *testing.T) { - assert.NoError(t, unittest.PrepareTestDatabase()) - pr := &PullRequest{ - ID: 1, - BaseBranch: "baseBranch", - HeadBranch: "headBranch", - } - assert.NoError(t, pr.UpdateCols("head_branch")) - - pr = unittest.AssertExistsAndLoadBean(t, &PullRequest{ID: 1}).(*PullRequest) - assert.Equal(t, "master", pr.BaseBranch) - assert.Equal(t, "headBranch", pr.HeadBranch) - unittest.CheckConsistencyFor(t, pr) -} - -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), - } - assert.NoError(t, 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()) -} - -// TODO TestAddTestPullRequestTask - -func TestPullRequest_IsWorkInProgress(t *testing.T) { - assert.NoError(t, unittest.PrepareTestDatabase()) - - pr := unittest.AssertExistsAndLoadBean(t, &PullRequest{ID: 2}).(*PullRequest) - pr.LoadIssue() - - assert.False(t, pr.IsWorkInProgress()) - - pr.Issue.Title = "WIP: " + pr.Issue.Title - assert.True(t, pr.IsWorkInProgress()) - - pr.Issue.Title = "[wip]: " + pr.Issue.Title - assert.True(t, pr.IsWorkInProgress()) -} - -func TestPullRequest_GetWorkInProgressPrefixWorkInProgress(t *testing.T) { - assert.NoError(t, unittest.PrepareTestDatabase()) - - pr := unittest.AssertExistsAndLoadBean(t, &PullRequest{ID: 2}).(*PullRequest) - pr.LoadIssue() - - assert.Empty(t, pr.GetWorkInProgressPrefix()) - - original := pr.Issue.Title - pr.Issue.Title = "WIP: " + original - assert.Equal(t, "WIP:", pr.GetWorkInProgressPrefix()) - - pr.Issue.Title = "[wip] " + original - assert.Equal(t, "[wip]", pr.GetWorkInProgressPrefix()) -} 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/review.go b/models/review.go deleted file mode 100644 index e92caba938..0000000000 --- a/models/review.go +++ /dev/null @@ -1,969 +0,0 @@ -// Copyright 2018 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" - "fmt" - "strings" - - "code.gitea.io/gitea/models/db" - git_model "code.gitea.io/gitea/models/git" - "code.gitea.io/gitea/models/organization" - "code.gitea.io/gitea/models/perm" - access_model "code.gitea.io/gitea/models/perm/access" - "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/timeutil" - - "xorm.io/builder" -) - -// ReviewType defines the sort of feedback a review gives -type ReviewType int - -// ReviewTypeUnknown unknown review type -const ReviewTypeUnknown ReviewType = -1 - -const ( - // ReviewTypePending is a review which is not published yet - ReviewTypePending ReviewType = iota - // ReviewTypeApprove approves changes - ReviewTypeApprove - // ReviewTypeComment gives general feedback - ReviewTypeComment - // ReviewTypeReject gives feedback blocking merge - ReviewTypeReject - // ReviewTypeRequest request review from others - ReviewTypeRequest -) - -// Icon returns the corresponding icon for the review type -func (rt ReviewType) Icon() string { - switch rt { - case ReviewTypeApprove: - return "check" - case ReviewTypeReject: - return "diff" - case ReviewTypeComment: - return "comment" - case ReviewTypeRequest: - return "dot-fill" - default: - return "comment" - } -} - -// Review represents collection of code comments giving feedback for a PR -type Review struct { - ID int64 `xorm:"pk autoincr"` - Type ReviewType - Reviewer *user_model.User `xorm:"-"` - ReviewerID int64 `xorm:"index"` - ReviewerTeamID int64 `xorm:"NOT NULL DEFAULT 0"` - ReviewerTeam *organization.Team `xorm:"-"` - OriginalAuthor string - OriginalAuthorID int64 - Issue *Issue `xorm:"-"` - IssueID int64 `xorm:"index"` - Content string `xorm:"TEXT"` - // Official is a review made by an assigned approver (counts towards approval) - Official bool `xorm:"NOT NULL DEFAULT false"` - CommitID string `xorm:"VARCHAR(40)"` - Stale bool `xorm:"NOT NULL DEFAULT false"` - Dismissed bool `xorm:"NOT NULL DEFAULT false"` - - CreatedUnix timeutil.TimeStamp `xorm:"INDEX created"` - UpdatedUnix timeutil.TimeStamp `xorm:"INDEX updated"` - - // CodeComments are the initial code comments of the review - CodeComments CodeComments `xorm:"-"` - - Comments []*Comment `xorm:"-"` -} - -func init() { - db.RegisterModel(new(Review)) -} - -// LoadCodeComments loads CodeComments -func (r *Review) LoadCodeComments(ctx context.Context) (err error) { - if r.CodeComments != nil { - return - } - if err = r.loadIssue(ctx); err != nil { - return - } - r.CodeComments, err = fetchCodeCommentsByReview(ctx, r.Issue, nil, r) - return -} - -func (r *Review) loadIssue(ctx context.Context) (err error) { - if r.Issue != nil { - return - } - r.Issue, err = getIssueByID(ctx, r.IssueID) - return -} - -func (r *Review) loadReviewer(ctx context.Context) (err error) { - if r.ReviewerID == 0 || r.Reviewer != nil { - return - } - r.Reviewer, err = user_model.GetUserByIDCtx(ctx, r.ReviewerID) - return -} - -func (r *Review) loadReviewerTeam(ctx context.Context) (err error) { - if r.ReviewerTeamID == 0 || r.ReviewerTeam != nil { - return - } - - r.ReviewerTeam, err = organization.GetTeamByID(ctx, r.ReviewerTeamID) - return -} - -// LoadReviewer loads reviewer -func (r *Review) LoadReviewer() error { - return r.loadReviewer(db.DefaultContext) -} - -// LoadReviewerTeam loads reviewer team -func (r *Review) LoadReviewerTeam() error { - return r.loadReviewerTeam(db.DefaultContext) -} - -// LoadAttributes loads all attributes except CodeComments -func (r *Review) LoadAttributes(ctx context.Context) (err error) { - if err = r.loadIssue(ctx); err != nil { - return - } - if err = r.LoadCodeComments(ctx); err != nil { - return - } - if err = r.loadReviewer(ctx); err != nil { - return - } - if err = r.loadReviewerTeam(ctx); err != nil { - return - } - return -} - -// GetReviewByID returns the review by the given ID -func GetReviewByID(ctx context.Context, id int64) (*Review, error) { - review := new(Review) - if has, err := db.GetEngine(ctx).ID(id).Get(review); err != nil { - return nil, err - } else if !has { - return nil, ErrReviewNotExist{ID: id} - } else { - return review, nil - } -} - -// FindReviewOptions represent possible filters to find reviews -type FindReviewOptions struct { - db.ListOptions - Type ReviewType - IssueID int64 - ReviewerID int64 - OfficialOnly bool -} - -func (opts *FindReviewOptions) toCond() builder.Cond { - cond := builder.NewCond() - if opts.IssueID > 0 { - cond = cond.And(builder.Eq{"issue_id": opts.IssueID}) - } - if opts.ReviewerID > 0 { - cond = cond.And(builder.Eq{"reviewer_id": opts.ReviewerID}) - } - if opts.Type != ReviewTypeUnknown { - cond = cond.And(builder.Eq{"type": opts.Type}) - } - if opts.OfficialOnly { - cond = cond.And(builder.Eq{"official": true}) - } - return cond -} - -// FindReviews returns reviews passing FindReviewOptions -func FindReviews(ctx context.Context, opts FindReviewOptions) ([]*Review, error) { - reviews := make([]*Review, 0, 10) - sess := db.GetEngine(ctx).Where(opts.toCond()) - if opts.Page > 0 { - sess = db.SetSessionPagination(sess, &opts) - } - return reviews, sess. - Asc("created_unix"). - Asc("id"). - Find(&reviews) -} - -// CountReviews returns count of reviews passing FindReviewOptions -func CountReviews(opts FindReviewOptions) (int64, error) { - return db.GetEngine(db.DefaultContext).Where(opts.toCond()).Count(&Review{}) -} - -// CreateReviewOptions represent the options to create a review. Type, Issue and Reviewer are required. -type CreateReviewOptions struct { - Content string - Type ReviewType - Issue *Issue - Reviewer *user_model.User - ReviewerTeam *organization.Team - Official bool - CommitID string - Stale bool -} - -// IsOfficialReviewer check if at least one of the provided reviewers can make official reviews in issue (counts towards required approvals) -func IsOfficialReviewer(ctx context.Context, issue *Issue, reviewers ...*user_model.User) (bool, error) { - pr, err := GetPullRequestByIssueID(ctx, issue.ID) - if err != nil { - return false, err - } - if err = pr.LoadProtectedBranchCtx(ctx); err != nil { - return false, err - } - if pr.ProtectedBranch == nil { - return false, nil - } - - for _, reviewer := range reviewers { - official, err := git_model.IsUserOfficialReviewerCtx(ctx, pr.ProtectedBranch, reviewer) - if official || err != nil { - return official, err - } - } - - return false, nil -} - -// IsOfficialReviewerTeam check if reviewer in this team can make official reviews in issue (counts towards required approvals) -func IsOfficialReviewerTeam(ctx context.Context, issue *Issue, team *organization.Team) (bool, error) { - pr, err := GetPullRequestByIssueID(ctx, issue.ID) - if err != nil { - return false, err - } - if err = pr.LoadProtectedBranchCtx(ctx); err != nil { - return false, err - } - if pr.ProtectedBranch == nil { - return false, nil - } - - if !pr.ProtectedBranch.EnableApprovalsWhitelist { - return team.UnitAccessModeCtx(ctx, unit.TypeCode) >= perm.AccessModeWrite, nil - } - - return base.Int64sContains(pr.ProtectedBranch.ApprovalsWhitelistTeamIDs, team.ID), nil -} - -// CreateReview creates a new review based on opts -func CreateReview(ctx context.Context, opts CreateReviewOptions) (*Review, error) { - review := &Review{ - Type: opts.Type, - Issue: opts.Issue, - IssueID: opts.Issue.ID, - Reviewer: opts.Reviewer, - ReviewerTeam: opts.ReviewerTeam, - Content: opts.Content, - Official: opts.Official, - CommitID: opts.CommitID, - Stale: opts.Stale, - } - if opts.Reviewer != nil { - review.ReviewerID = opts.Reviewer.ID - } else { - if review.Type != ReviewTypeRequest { - review.Type = ReviewTypeRequest - } - review.ReviewerTeamID = opts.ReviewerTeam.ID - } - return review, db.Insert(ctx, review) -} - -// GetCurrentReview returns the current pending review of reviewer for given issue -func GetCurrentReview(ctx context.Context, reviewer *user_model.User, issue *Issue) (*Review, error) { - if reviewer == nil { - return nil, nil - } - reviews, err := FindReviews(ctx, FindReviewOptions{ - Type: ReviewTypePending, - IssueID: issue.ID, - ReviewerID: reviewer.ID, - }) - if err != nil { - return nil, err - } - if len(reviews) == 0 { - return nil, ErrReviewNotExist{} - } - reviews[0].Reviewer = reviewer - reviews[0].Issue = issue - return reviews[0], nil -} - -// ReviewExists returns whether a review exists for a particular line of code in the PR -func ReviewExists(issue *Issue, treePath string, line int64) (bool, error) { - return db.GetEngine(db.DefaultContext).Cols("id").Exist(&Comment{IssueID: issue.ID, TreePath: treePath, Line: line, Type: CommentTypeCode}) -} - -// ContentEmptyErr represents an content empty error -type ContentEmptyErr struct{} - -func (ContentEmptyErr) Error() string { - return "Review content is empty" -} - -// IsContentEmptyErr returns true if err is a ContentEmptyErr -func IsContentEmptyErr(err error) bool { - _, ok := err.(ContentEmptyErr) - return ok -} - -// SubmitReview creates a review out of the existing pending review or creates a new one if no pending review exist -func SubmitReview(doer *user_model.User, issue *Issue, reviewType ReviewType, content, commitID string, stale bool, attachmentUUIDs []string) (*Review, *Comment, error) { - ctx, committer, err := db.TxContext() - if err != nil { - return nil, nil, err - } - defer committer.Close() - sess := db.GetEngine(ctx) - - official := false - - review, err := GetCurrentReview(ctx, doer, issue) - if err != nil { - if !IsErrReviewNotExist(err) { - return nil, nil, err - } - - if reviewType != ReviewTypeApprove && len(strings.TrimSpace(content)) == 0 { - return nil, nil, ContentEmptyErr{} - } - - if reviewType == ReviewTypeApprove || reviewType == ReviewTypeReject { - // Only reviewers latest review of type approve and reject shall count as "official", so existing reviews needs to be cleared - if _, err := db.Exec(ctx, "UPDATE `review` SET official=? WHERE issue_id=? AND reviewer_id=?", false, issue.ID, doer.ID); err != nil { - return nil, nil, err - } - if official, err = IsOfficialReviewer(ctx, issue, doer); err != nil { - return nil, nil, err - } - } - - // No current review. Create a new one! - if review, err = CreateReview(ctx, CreateReviewOptions{ - Type: reviewType, - Issue: issue, - Reviewer: doer, - Content: content, - Official: official, - CommitID: commitID, - Stale: stale, - }); err != nil { - return nil, nil, err - } - } else { - if err := review.LoadCodeComments(ctx); err != nil { - return nil, nil, err - } - if reviewType != ReviewTypeApprove && len(review.CodeComments) == 0 && len(strings.TrimSpace(content)) == 0 { - return nil, nil, ContentEmptyErr{} - } - - if reviewType == ReviewTypeApprove || reviewType == ReviewTypeReject { - // Only reviewers latest review of type approve and reject shall count as "official", so existing reviews needs to be cleared - if _, err := db.Exec(ctx, "UPDATE `review` SET official=? WHERE issue_id=? AND reviewer_id=?", false, issue.ID, doer.ID); err != nil { - return nil, nil, err - } - if official, err = IsOfficialReviewer(ctx, issue, doer); err != nil { - return nil, nil, err - } - } - - review.Official = official - review.Issue = issue - review.Content = content - review.Type = reviewType - review.CommitID = commitID - review.Stale = stale - - if _, err := sess.ID(review.ID).Cols("content, type, official, commit_id, stale").Update(review); err != nil { - return nil, nil, err - } - } - - comm, err := CreateCommentCtx(ctx, &CreateCommentOptions{ - Type: CommentTypeReview, - Doer: doer, - Content: review.Content, - Issue: issue, - Repo: issue.Repo, - ReviewID: review.ID, - Attachments: attachmentUUIDs, - }) - if err != nil || comm == nil { - return nil, nil, err - } - - // try to remove team review request if need - if issue.Repo.Owner.IsOrganization() && (reviewType == ReviewTypeApprove || reviewType == ReviewTypeReject) { - teamReviewRequests := make([]*Review, 0, 10) - if err := sess.SQL("SELECT * FROM review WHERE issue_id = ? AND reviewer_team_id > 0 AND type = ?", issue.ID, ReviewTypeRequest).Find(&teamReviewRequests); err != nil { - return nil, nil, err - } - - for _, teamReviewRequest := range teamReviewRequests { - ok, err := organization.IsTeamMember(ctx, issue.Repo.OwnerID, teamReviewRequest.ReviewerTeamID, doer.ID) - if err != nil { - return nil, nil, err - } else if !ok { - continue - } - - if _, err := sess.ID(teamReviewRequest.ID).NoAutoCondition().Delete(teamReviewRequest); err != nil { - return nil, nil, err - } - } - } - - comm.Review = review - return review, comm, committer.Commit() -} - -// GetReviewersByIssueID gets the latest review of each reviewer for a pull request -func GetReviewersByIssueID(issueID int64) ([]*Review, error) { - reviews := make([]*Review, 0, 10) - - sess := db.GetEngine(db.DefaultContext) - - // Get latest review of each reviewer, sorted in order they were made - if err := sess.SQL("SELECT * FROM review WHERE id IN (SELECT max(id) as id FROM review WHERE issue_id = ? AND reviewer_team_id = 0 AND type in (?, ?, ?) AND dismissed = ? AND original_author_id = 0 GROUP BY issue_id, reviewer_id) ORDER BY review.updated_unix ASC", - issueID, ReviewTypeApprove, ReviewTypeReject, ReviewTypeRequest, false). - Find(&reviews); err != nil { - return nil, err - } - - teamReviewRequests := make([]*Review, 0, 5) - if err := sess.SQL("SELECT * FROM review WHERE id IN (SELECT max(id) as id FROM review WHERE issue_id = ? AND reviewer_team_id <> 0 AND original_author_id = 0 GROUP BY issue_id, reviewer_team_id) ORDER BY review.updated_unix ASC", - issueID). - Find(&teamReviewRequests); err != nil { - return nil, err - } - - if len(teamReviewRequests) > 0 { - reviews = append(reviews, teamReviewRequests...) - } - - return reviews, nil -} - -// GetReviewersFromOriginalAuthorsByIssueID gets the latest review of each original authors for a pull request -func GetReviewersFromOriginalAuthorsByIssueID(issueID int64) ([]*Review, error) { - reviews := make([]*Review, 0, 10) - - // Get latest review of each reviewer, sorted in order they were made - if err := db.GetEngine(db.DefaultContext).SQL("SELECT * FROM review WHERE id IN (SELECT max(id) as id FROM review WHERE issue_id = ? AND reviewer_team_id = 0 AND type in (?, ?, ?) AND original_author_id <> 0 GROUP BY issue_id, original_author_id) ORDER BY review.updated_unix ASC", - issueID, ReviewTypeApprove, ReviewTypeReject, ReviewTypeRequest). - Find(&reviews); err != nil { - return nil, err - } - - return reviews, nil -} - -// GetReviewByIssueIDAndUserID get the latest review of reviewer for a pull request -func GetReviewByIssueIDAndUserID(ctx context.Context, issueID, userID int64) (*Review, error) { - review := new(Review) - - has, err := db.GetEngine(ctx).SQL("SELECT * FROM review WHERE id IN (SELECT max(id) as id FROM review WHERE issue_id = ? AND reviewer_id = ? AND original_author_id = 0 AND type in (?, ?, ?))", - issueID, userID, ReviewTypeApprove, ReviewTypeReject, ReviewTypeRequest). - Get(review) - if err != nil { - return nil, err - } - - if !has { - return nil, ErrReviewNotExist{} - } - - return review, nil -} - -// GetTeamReviewerByIssueIDAndTeamID get the latest review request of reviewer team for a pull request -func GetTeamReviewerByIssueIDAndTeamID(ctx context.Context, issueID, teamID int64) (review *Review, err error) { - review = new(Review) - - has := false - if has, err = db.GetEngine(ctx).SQL("SELECT * FROM review WHERE id IN (SELECT max(id) as id FROM review WHERE issue_id = ? AND reviewer_team_id = ?)", - issueID, teamID). - Get(review); err != nil { - return nil, err - } - - if !has { - return nil, ErrReviewNotExist{0} - } - - return -} - -// MarkReviewsAsStale marks existing reviews as stale -func MarkReviewsAsStale(issueID int64) (err error) { - _, err = db.GetEngine(db.DefaultContext).Exec("UPDATE `review` SET stale=? WHERE issue_id=?", true, issueID) - - return -} - -// MarkReviewsAsNotStale marks existing reviews as not stale for a giving commit SHA -func MarkReviewsAsNotStale(issueID int64, commitID string) (err error) { - _, err = db.GetEngine(db.DefaultContext).Exec("UPDATE `review` SET stale=? WHERE issue_id=? AND commit_id=?", false, issueID, commitID) - - return -} - -// DismissReview change the dismiss status of a review -func DismissReview(review *Review, isDismiss bool) (err error) { - if review.Dismissed == isDismiss || (review.Type != ReviewTypeApprove && review.Type != ReviewTypeReject) { - return nil - } - - review.Dismissed = isDismiss - - if review.ID == 0 { - return ErrReviewNotExist{} - } - - _, err = db.GetEngine(db.DefaultContext).ID(review.ID).Cols("dismissed").Update(review) - - return -} - -// InsertReviews inserts review and review comments -func InsertReviews(reviews []*Review) error { - ctx, committer, err := db.TxContext() - if err != nil { - return err - } - defer committer.Close() - sess := db.GetEngine(ctx) - - for _, review := range reviews { - if _, err := sess.NoAutoTime().Insert(review); err != nil { - return err - } - - if _, err := sess.NoAutoTime().Insert(&Comment{ - Type: CommentTypeReview, - Content: review.Content, - PosterID: review.ReviewerID, - OriginalAuthor: review.OriginalAuthor, - OriginalAuthorID: review.OriginalAuthorID, - IssueID: review.IssueID, - ReviewID: review.ID, - CreatedUnix: review.CreatedUnix, - UpdatedUnix: review.UpdatedUnix, - }); err != nil { - return err - } - - for _, c := range review.Comments { - c.ReviewID = review.ID - } - - if len(review.Comments) > 0 { - if _, err := sess.NoAutoTime().Insert(review.Comments); err != nil { - return err - } - } - } - - return committer.Commit() -} - -// AddReviewRequest add a review request from one reviewer -func AddReviewRequest(issue *Issue, reviewer, doer *user_model.User) (*Comment, error) { - ctx, committer, err := db.TxContext() - if err != nil { - return nil, err - } - defer committer.Close() - sess := db.GetEngine(ctx) - - review, err := GetReviewByIssueIDAndUserID(ctx, issue.ID, reviewer.ID) - if err != nil && !IsErrReviewNotExist(err) { - return nil, err - } - - // skip it when reviewer hase been request to review - if review != nil && review.Type == ReviewTypeRequest { - return nil, nil - } - - official, err := IsOfficialReviewer(ctx, issue, reviewer, doer) - if err != nil { - return nil, err - } else if official { - if _, err := sess.Exec("UPDATE `review` SET official=? WHERE issue_id=? AND reviewer_id=?", false, issue.ID, reviewer.ID); err != nil { - return nil, err - } - } - - review, err = CreateReview(ctx, CreateReviewOptions{ - Type: ReviewTypeRequest, - Issue: issue, - Reviewer: reviewer, - Official: official, - Stale: false, - }) - if err != nil { - return nil, err - } - - comment, err := CreateCommentCtx(ctx, &CreateCommentOptions{ - Type: CommentTypeReviewRequest, - Doer: doer, - Repo: issue.Repo, - Issue: issue, - RemovedAssignee: false, // Use RemovedAssignee as !isRequest - AssigneeID: reviewer.ID, // Use AssigneeID as reviewer ID - ReviewID: review.ID, - }) - if err != nil { - return nil, err - } - - return comment, committer.Commit() -} - -// RemoveReviewRequest remove a review request from one reviewer -func RemoveReviewRequest(issue *Issue, reviewer, doer *user_model.User) (*Comment, error) { - ctx, committer, err := db.TxContext() - if err != nil { - return nil, err - } - defer committer.Close() - - review, err := GetReviewByIssueIDAndUserID(ctx, issue.ID, reviewer.ID) - if err != nil && !IsErrReviewNotExist(err) { - return nil, err - } - - if review == nil || review.Type != ReviewTypeRequest { - return nil, nil - } - - if _, err = db.DeleteByBean(ctx, review); err != nil { - return nil, err - } - - official, err := IsOfficialReviewer(ctx, issue, reviewer) - if err != nil { - return nil, err - } else if official { - // recalculate the latest official review for reviewer - review, err := GetReviewByIssueIDAndUserID(ctx, issue.ID, reviewer.ID) - if err != nil && !IsErrReviewNotExist(err) { - return nil, err - } - - if review != nil { - if _, err := db.Exec(ctx, "UPDATE `review` SET official=? WHERE id=?", true, review.ID); err != nil { - return nil, err - } - } - } - - comment, err := CreateCommentCtx(ctx, &CreateCommentOptions{ - Type: CommentTypeReviewRequest, - Doer: doer, - Repo: issue.Repo, - Issue: issue, - RemovedAssignee: true, // Use RemovedAssignee as !isRequest - AssigneeID: reviewer.ID, // Use AssigneeID as reviewer ID - }) - if err != nil { - return nil, err - } - - return comment, committer.Commit() -} - -// AddTeamReviewRequest add a review request from one team -func AddTeamReviewRequest(issue *Issue, reviewer *organization.Team, doer *user_model.User) (*Comment, error) { - ctx, committer, err := db.TxContext() - if err != nil { - return nil, err - } - defer committer.Close() - - review, err := GetTeamReviewerByIssueIDAndTeamID(ctx, issue.ID, reviewer.ID) - if err != nil && !IsErrReviewNotExist(err) { - return nil, err - } - - // This team already has been requested to review - therefore skip this. - if review != nil { - return nil, nil - } - - official, err := IsOfficialReviewerTeam(ctx, issue, reviewer) - if err != nil { - return nil, fmt.Errorf("isOfficialReviewerTeam(): %v", err) - } else if !official { - if official, err = IsOfficialReviewer(ctx, issue, doer); err != nil { - return nil, fmt.Errorf("isOfficialReviewer(): %v", err) - } - } - - if review, err = CreateReview(ctx, CreateReviewOptions{ - Type: ReviewTypeRequest, - Issue: issue, - ReviewerTeam: reviewer, - Official: official, - Stale: false, - }); err != nil { - return nil, err - } - - if official { - if _, err := db.Exec(ctx, "UPDATE `review` SET official=? WHERE issue_id=? AND reviewer_team_id=?", false, issue.ID, reviewer.ID); err != nil { - return nil, err - } - } - - comment, err := CreateCommentCtx(ctx, &CreateCommentOptions{ - Type: CommentTypeReviewRequest, - Doer: doer, - Repo: issue.Repo, - Issue: issue, - RemovedAssignee: false, // Use RemovedAssignee as !isRequest - AssigneeTeamID: reviewer.ID, // Use AssigneeTeamID as reviewer team ID - ReviewID: review.ID, - }) - if err != nil { - return nil, fmt.Errorf("CreateCommentCtx(): %v", err) - } - - return comment, committer.Commit() -} - -// RemoveTeamReviewRequest remove a review request from one team -func RemoveTeamReviewRequest(issue *Issue, reviewer *organization.Team, doer *user_model.User) (*Comment, error) { - ctx, committer, err := db.TxContext() - if err != nil { - return nil, err - } - defer committer.Close() - - review, err := GetTeamReviewerByIssueIDAndTeamID(ctx, issue.ID, reviewer.ID) - if err != nil && !IsErrReviewNotExist(err) { - return nil, err - } - - if review == nil { - return nil, nil - } - - if _, err = db.DeleteByBean(ctx, review); err != nil { - return nil, err - } - - official, err := IsOfficialReviewerTeam(ctx, issue, reviewer) - if err != nil { - return nil, fmt.Errorf("isOfficialReviewerTeam(): %v", err) - } - - if official { - // recalculate which is the latest official review from that team - review, err := GetReviewByIssueIDAndUserID(ctx, issue.ID, -reviewer.ID) - if err != nil && !IsErrReviewNotExist(err) { - return nil, err - } - - if review != nil { - if _, err := db.Exec(ctx, "UPDATE `review` SET official=? WHERE id=?", true, review.ID); err != nil { - return nil, err - } - } - } - - if doer == nil { - return nil, committer.Commit() - } - - comment, err := CreateCommentCtx(ctx, &CreateCommentOptions{ - Type: CommentTypeReviewRequest, - Doer: doer, - Repo: issue.Repo, - Issue: issue, - RemovedAssignee: true, // Use RemovedAssignee as !isRequest - AssigneeTeamID: reviewer.ID, // Use AssigneeTeamID as reviewer team ID - }) - if err != nil { - return nil, fmt.Errorf("CreateCommentCtx(): %v", err) - } - - return comment, committer.Commit() -} - -// MarkConversation Add or remove Conversation mark for a code comment -func MarkConversation(comment *Comment, doer *user_model.User, isResolve bool) (err error) { - if comment.Type != CommentTypeCode { - return nil - } - - if isResolve { - if comment.ResolveDoerID != 0 { - return nil - } - - if _, err = db.GetEngine(db.DefaultContext).Exec("UPDATE `comment` SET resolve_doer_id=? WHERE id=?", doer.ID, comment.ID); err != nil { - return err - } - } else { - if comment.ResolveDoerID == 0 { - return nil - } - - if _, err = db.GetEngine(db.DefaultContext).Exec("UPDATE `comment` SET resolve_doer_id=? WHERE id=?", 0, comment.ID); err != nil { - return err - } - } - - return nil -} - -// CanMarkConversation Add or remove Conversation mark for a code comment permission check -// the PR writer , offfcial reviewer and poster can do it -func CanMarkConversation(issue *Issue, doer *user_model.User) (permResult bool, err error) { - if doer == nil || issue == nil { - return false, fmt.Errorf("issue or doer is nil") - } - - if doer.ID != issue.PosterID { - if err = issue.LoadRepo(db.DefaultContext); err != nil { - return false, err - } - - p, err := access_model.GetUserRepoPermission(db.DefaultContext, issue.Repo, doer) - if err != nil { - return false, err - } - - permResult = p.CanAccess(perm.AccessModeWrite, unit.TypePullRequests) - if !permResult { - if permResult, err = IsOfficialReviewer(db.DefaultContext, issue, doer); err != nil { - return false, err - } - } - - if !permResult { - return false, nil - } - } - - return true, nil -} - -// DeleteReview delete a review and it's code comments -func DeleteReview(r *Review) error { - ctx, committer, err := db.TxContext() - if err != nil { - return err - } - defer committer.Close() - sess := db.GetEngine(ctx) - - if r.ID == 0 { - return fmt.Errorf("review is not allowed to be 0") - } - - if r.Type == ReviewTypeRequest { - return fmt.Errorf("review request can not be deleted using this method") - } - - opts := FindCommentsOptions{ - Type: CommentTypeCode, - IssueID: r.IssueID, - ReviewID: r.ID, - } - - if _, err := sess.Where(opts.toConds()).Delete(new(Comment)); err != nil { - return err - } - - opts = FindCommentsOptions{ - Type: CommentTypeReview, - IssueID: r.IssueID, - ReviewID: r.ID, - } - - if _, err := sess.Where(opts.toConds()).Delete(new(Comment)); err != nil { - return err - } - - if _, err := sess.ID(r.ID).Delete(new(Review)); err != nil { - return err - } - - return committer.Commit() -} - -// GetCodeCommentsCount return count of CodeComments a Review has -func (r *Review) GetCodeCommentsCount() int { - opts := FindCommentsOptions{ - Type: CommentTypeCode, - IssueID: r.IssueID, - ReviewID: r.ID, - } - conds := opts.toConds() - if r.ID == 0 { - conds = conds.And(builder.Eq{"invalidated": false}) - } - - count, err := db.GetEngine(db.DefaultContext).Where(conds).Count(new(Comment)) - if err != nil { - return 0 - } - return int(count) -} - -// HTMLURL formats a URL-string to the related review issue-comment -func (r *Review) HTMLURL() string { - opts := FindCommentsOptions{ - Type: CommentTypeReview, - IssueID: r.IssueID, - ReviewID: r.ID, - } - comment := new(Comment) - has, err := db.GetEngine(db.DefaultContext).Where(opts.toConds()).Get(comment) - if err != nil || !has { - return "" - } - return comment.HTMLURL() -} - -// RemapExternalUser ExternalUserRemappable interface -func (r *Review) RemapExternalUser(externalName string, externalID, userID int64) error { - r.OriginalAuthor = externalName - r.OriginalAuthorID = externalID - r.ReviewerID = userID - return nil -} - -// GetUserID ExternalUserRemappable interface -func (r *Review) GetUserID() int64 { return r.ReviewerID } - -// GetExternalName ExternalUserRemappable interface -func (r *Review) GetExternalName() string { return r.OriginalAuthor } - -// GetExternalID ExternalUserRemappable interface -func (r *Review) GetExternalID() int64 { return r.OriginalAuthorID } diff --git a/models/review_test.go b/models/review_test.go deleted file mode 100644 index 93291f9f57..0000000000 --- a/models/review_test.go +++ /dev/null @@ -1,202 +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" - - "github.com/stretchr/testify/assert" -) - -func TestGetReviewByID(t *testing.T) { - assert.NoError(t, unittest.PrepareTestDatabase()) - review, err := GetReviewByID(db.DefaultContext, 1) - assert.NoError(t, err) - assert.Equal(t, "Demo Review", review.Content) - assert.Equal(t, ReviewTypeApprove, review.Type) - - _, err = GetReviewByID(db.DefaultContext, 23892) - assert.Error(t, err) - assert.True(t, IsErrReviewNotExist(err), "IsErrReviewNotExist") -} - -func TestReview_LoadAttributes(t *testing.T) { - assert.NoError(t, unittest.PrepareTestDatabase()) - review := unittest.AssertExistsAndLoadBean(t, &Review{ID: 1}).(*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) - assert.Error(t, invalidReview1.LoadAttributes(db.DefaultContext)) - - invalidReview2 := unittest.AssertExistsAndLoadBean(t, &Review{ID: 3}).(*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) - assert.NoError(t, review.LoadAttributes(db.DefaultContext)) - assert.NoError(t, review.LoadCodeComments(db.DefaultContext)) - assert.Len(t, review.CodeComments, 1) - assert.Equal(t, int64(4), review.CodeComments["README.md"][int64(4)][0].Line) -} - -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()) -} - -func TestFindReviews(t *testing.T) { - assert.NoError(t, unittest.PrepareTestDatabase()) - reviews, err := FindReviews(db.DefaultContext, FindReviewOptions{ - Type: ReviewTypeApprove, - IssueID: 2, - ReviewerID: 1, - }) - assert.NoError(t, err) - assert.Len(t, reviews, 1) - assert.Equal(t, "Demo Review", reviews[0].Content) -} - -func TestGetCurrentReview(t *testing.T) { - assert.NoError(t, unittest.PrepareTestDatabase()) - issue := unittest.AssertExistsAndLoadBean(t, &Issue{ID: 2}).(*Issue) - user := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 1}).(*user_model.User) - - review, err := GetCurrentReview(db.DefaultContext, user, issue) - assert.NoError(t, err) - assert.NotNil(t, review) - assert.Equal(t, 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) - assert.Error(t, err) - assert.True(t, IsErrReviewNotExist(err)) - assert.Nil(t, review2) -} - -func TestCreateReview(t *testing.T) { - assert.NoError(t, unittest.PrepareTestDatabase()) - - issue := unittest.AssertExistsAndLoadBean(t, &Issue{ID: 2}).(*Issue) - user := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 1}).(*user_model.User) - - review, err := CreateReview(db.DefaultContext, CreateReviewOptions{ - Content: "New Review", - Type: ReviewTypePending, - Issue: issue, - Reviewer: user, - }) - assert.NoError(t, err) - assert.Equal(t, "New Review", review.Content) - unittest.AssertExistsAndLoadBean(t, &Review{Content: "New Review"}) -} - -func TestGetReviewersByIssueID(t *testing.T) { - assert.NoError(t, unittest.PrepareTestDatabase()) - - issue := unittest.AssertExistsAndLoadBean(t, &Issue{ID: 3}).(*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 = append(expectedReviews, - &Review{ - Reviewer: user3, - Type: ReviewTypeReject, - UpdatedUnix: 946684812, - }, - &Review{ - Reviewer: user4, - Type: ReviewTypeApprove, - UpdatedUnix: 946684813, - }, - &Review{ - Reviewer: user2, - Type: ReviewTypeReject, - UpdatedUnix: 946684814, - }) - - allReviews, err := GetReviewersByIssueID(issue.ID) - for _, reviewer := range allReviews { - assert.NoError(t, reviewer.LoadReviewer()) - } - assert.NoError(t, err) - if assert.Len(t, allReviews, 3) { - for i, review := range allReviews { - assert.Equal(t, expectedReviews[i].Reviewer, review.Reviewer) - assert.Equal(t, expectedReviews[i].Type, review.Type) - assert.Equal(t, expectedReviews[i].UpdatedUnix, review.UpdatedUnix) - } - } -} - -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) - 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.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.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.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.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.True(t, rejectReviewExample.Dismissed) - assert.False(t, requestReviewExample.Dismissed) - assert.False(t, approveReviewExample.Dismissed) - - assert.NoError(t, 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.False(t, rejectReviewExample.Dismissed) - assert.False(t, requestReviewExample.Dismissed) - assert.True(t, approveReviewExample.Dismissed) -} 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) } diff --git a/modules/context/repo.go b/modules/context/repo.go index 05ced909a8..c2b8306b9d 100644 --- a/modules/context/repo.go +++ b/modules/context/repo.go @@ -18,6 +18,7 @@ import ( "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" access_model "code.gitea.io/gitea/models/perm/access" repo_model "code.gitea.io/gitea/models/repo" unit_model "code.gitea.io/gitea/models/unit" @@ -83,7 +84,7 @@ type Repository struct { // CanWriteToBranch checks if the branch is writable by the user func (r *Repository) CanWriteToBranch(user *user_model.User, branch string) bool { - return models.CanMaintainerWriteToBranch(r.Permission, branch, user) + return issues_model.CanMaintainerWriteToBranch(r.Permission, branch, user) } // CanEnableEditor returns true if repository is editable and user has proper access level. @@ -158,11 +159,11 @@ func (r *Repository) CanCommitToBranch(ctx context.Context, doer *user_model.Use } // CanUseTimetracker returns whether or not a user can use the timetracker. -func (r *Repository) CanUseTimetracker(issue *models.Issue, user *user_model.User) bool { +func (r *Repository) CanUseTimetracker(issue *issues_model.Issue, user *user_model.User) bool { // Checking for following: // 1. Is timetracker enabled // 2. Is the user a contributor, admin, poster or assignee and do the repository policies require this? - isAssigned, _ := models.IsUserAssignedToIssue(db.DefaultContext, issue, user) + isAssigned, _ := issues_model.IsUserAssignedToIssue(db.DefaultContext, issue, user) return r.Repository.IsTimetrackerEnabled() && (!r.Repository.AllowOnlyContributorsToTrackTime() || r.Permission.CanWriteIssuesOrPulls(issue.IsPull) || issue.IsPoster(user.ID) || isAssigned) } diff --git a/modules/convert/convert.go b/modules/convert/convert.go index 4e8aa59067..c8cb23261e 100644 --- a/modules/convert/convert.go +++ b/modules/convert/convert.go @@ -11,11 +11,11 @@ import ( "strings" "time" - "code.gitea.io/gitea/models" asymkey_model "code.gitea.io/gitea/models/asymkey" "code.gitea.io/gitea/models/auth" "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" "code.gitea.io/gitea/models/perm" access_model "code.gitea.io/gitea/models/perm/access" @@ -55,7 +55,7 @@ func ToBranch(repo *repo_model.Repository, b *git.Branch, c *git.Commit, bp *git if err != nil { return nil, err } - canPush = models.CanMaintainerWriteToBranch(perms, b.Name, user) + canPush = issues_model.CanMaintainerWriteToBranch(perms, b.Name, user) } return &api.Branch{ diff --git a/modules/convert/issue.go b/modules/convert/issue.go index a4512e424f..35eff05229 100644 --- a/modules/convert/issue.go +++ b/modules/convert/issue.go @@ -9,7 +9,6 @@ import ( "net/url" "strings" - "code.gitea.io/gitea/models" "code.gitea.io/gitea/models/db" issues_model "code.gitea.io/gitea/models/issues" repo_model "code.gitea.io/gitea/models/repo" @@ -23,7 +22,7 @@ import ( // it assumes some fields assigned with values: // Required - Poster, Labels, // Optional - Milestone, Assignee, PullRequest -func ToAPIIssue(issue *models.Issue) *api.Issue { +func ToAPIIssue(issue *issues_model.Issue) *api.Issue { if err := issue.LoadLabels(db.DefaultContext); err != nil { return &api.Issue{} } @@ -100,7 +99,7 @@ func ToAPIIssue(issue *models.Issue) *api.Issue { } // ToAPIIssueList converts an IssueList to API format -func ToAPIIssueList(il models.IssueList) []*api.Issue { +func ToAPIIssueList(il issues_model.IssueList) []*api.Issue { result := make([]*api.Issue, len(il)) for i := range il { result[i] = ToAPIIssue(il[i]) @@ -109,7 +108,7 @@ func ToAPIIssueList(il models.IssueList) []*api.Issue { } // ToTrackedTime converts TrackedTime to API format -func ToTrackedTime(t *models.TrackedTime) (apiT *api.TrackedTime) { +func ToTrackedTime(t *issues_model.TrackedTime) (apiT *api.TrackedTime) { apiT = &api.TrackedTime{ ID: t.ID, IssueID: t.IssueID, @@ -128,13 +127,13 @@ func ToTrackedTime(t *models.TrackedTime) (apiT *api.TrackedTime) { } // ToStopWatches convert Stopwatch list to api.StopWatches -func ToStopWatches(sws []*models.Stopwatch) (api.StopWatches, error) { +func ToStopWatches(sws []*issues_model.Stopwatch) (api.StopWatches, error) { result := api.StopWatches(make([]api.StopWatch, 0, len(sws))) - issueCache := make(map[int64]*models.Issue) + issueCache := make(map[int64]*issues_model.Issue) repoCache := make(map[int64]*repo_model.Repository) var ( - issue *models.Issue + issue *issues_model.Issue repo *repo_model.Repository ok bool err error @@ -143,7 +142,7 @@ func ToStopWatches(sws []*models.Stopwatch) (api.StopWatches, error) { for _, sw := range sws { issue, ok = issueCache[sw.IssueID] if !ok { - issue, err = models.GetIssueByID(sw.IssueID) + issue, err = issues_model.GetIssueByID(db.DefaultContext, sw.IssueID) if err != nil { return nil, err } @@ -170,7 +169,7 @@ func ToStopWatches(sws []*models.Stopwatch) (api.StopWatches, error) { } // ToTrackedTimeList converts TrackedTimeList to API format -func ToTrackedTimeList(tl models.TrackedTimeList) api.TrackedTimeList { +func ToTrackedTimeList(tl issues_model.TrackedTimeList) api.TrackedTimeList { result := make([]*api.TrackedTime, 0, len(tl)) for _, t := range tl { result = append(result, ToTrackedTime(t)) @@ -179,7 +178,7 @@ func ToTrackedTimeList(tl models.TrackedTimeList) api.TrackedTimeList { } // ToLabel converts Label to API format -func ToLabel(label *models.Label, repo *repo_model.Repository, org *user_model.User) *api.Label { +func ToLabel(label *issues_model.Label, repo *repo_model.Repository, org *user_model.User) *api.Label { result := &api.Label{ ID: label.ID, Name: label.Name, @@ -206,7 +205,7 @@ func ToLabel(label *models.Label, repo *repo_model.Repository, org *user_model.U } // ToLabelList converts list of Label to API format -func ToLabelList(labels []*models.Label, repo *repo_model.Repository, org *user_model.User) []*api.Label { +func ToLabelList(labels []*issues_model.Label, repo *repo_model.Repository, org *user_model.User) []*api.Label { result := make([]*api.Label, len(labels)) for i := range labels { result[i] = ToLabel(labels[i], repo, org) diff --git a/modules/convert/issue_comment.go b/modules/convert/issue_comment.go index eaa7f64ea3..ccc94b2496 100644 --- a/modules/convert/issue_comment.go +++ b/modules/convert/issue_comment.go @@ -5,16 +5,16 @@ package convert import ( - "code.gitea.io/gitea/models" "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/log" api "code.gitea.io/gitea/modules/structs" ) -// ToComment converts a models.Comment to the api.Comment format -func ToComment(c *models.Comment) *api.Comment { +// ToComment converts a issues_model.Comment to the api.Comment format +func ToComment(c *issues_model.Comment) *api.Comment { return &api.Comment{ ID: c.ID, Poster: ToUser(c.Poster, nil), @@ -27,8 +27,8 @@ func ToComment(c *models.Comment) *api.Comment { } } -// ToTimelineComment converts a models.Comment to the api.TimelineComment format -func ToTimelineComment(c *models.Comment, doer *user_model.User) *api.TimelineComment { +// ToTimelineComment converts a issues_model.Comment to the api.TimelineComment format +func ToTimelineComment(c *issues_model.Comment, doer *user_model.User) *api.TimelineComment { err := c.LoadMilestone() if err != nil { log.Error("LoadMilestone: %v", err) @@ -105,7 +105,7 @@ func ToTimelineComment(c *models.Comment, doer *user_model.User) *api.TimelineCo } if c.RefIssueID != 0 { - issue, err := models.GetIssueByID(c.RefIssueID) + issue, err := issues_model.GetIssueByID(db.DefaultContext, c.RefIssueID) if err != nil { log.Error("GetIssueByID(%d): %v", c.RefIssueID, err) return nil @@ -114,7 +114,7 @@ func ToTimelineComment(c *models.Comment, doer *user_model.User) *api.TimelineCo } if c.RefCommentID != 0 { - com, err := models.GetCommentByID(db.DefaultContext, c.RefCommentID) + com, err := issues_model.GetCommentByID(db.DefaultContext, c.RefCommentID) if err != nil { log.Error("GetCommentByID(%d): %v", c.RefCommentID, err) return nil diff --git a/modules/convert/issue_test.go b/modules/convert/issue_test.go index b237c18f69..5bf04bcb52 100644 --- a/modules/convert/issue_test.go +++ b/modules/convert/issue_test.go @@ -9,7 +9,6 @@ import ( "testing" "time" - "code.gitea.io/gitea/models" issues_model "code.gitea.io/gitea/models/issues" repo_model "code.gitea.io/gitea/models/repo" "code.gitea.io/gitea/models/unittest" @@ -22,7 +21,7 @@ import ( func TestLabel_ToLabel(t *testing.T) { assert.NoError(t, unittest.PrepareTestDatabase()) - label := unittest.AssertExistsAndLoadBean(t, &models.Label{ID: 1}).(*models.Label) + label := unittest.AssertExistsAndLoadBean(t, &issues_model.Label{ID: 1}).(*issues_model.Label) repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: label.RepoID}).(*repo_model.Repository) assert.Equal(t, &api.Label{ ID: label.ID, diff --git a/modules/convert/pull.go b/modules/convert/pull.go index 310a7626c9..9c31f9bd2c 100644 --- a/modules/convert/pull.go +++ b/modules/convert/pull.go @@ -8,7 +8,7 @@ import ( "context" "fmt" - "code.gitea.io/gitea/models" + issues_model "code.gitea.io/gitea/models/issues" "code.gitea.io/gitea/models/perm" access_model "code.gitea.io/gitea/models/perm/access" user_model "code.gitea.io/gitea/models/user" @@ -20,7 +20,7 @@ import ( // ToAPIPullRequest assumes following fields have been assigned with valid values: // Required - Issue // Optional - Merger -func ToAPIPullRequest(ctx context.Context, pr *models.PullRequest, doer *user_model.User) *api.PullRequest { +func ToAPIPullRequest(ctx context.Context, pr *issues_model.PullRequest, doer *user_model.User) *api.PullRequest { var ( baseBranch *git.Branch headBranch *git.Branch @@ -114,7 +114,7 @@ func ToAPIPullRequest(ctx context.Context, pr *models.PullRequest, doer *user_mo } } - if pr.Flow == models.PullRequestFlowAGit { + if pr.Flow == issues_model.PullRequestFlowAGit { gitRepo, err := git.OpenRepository(ctx, pr.BaseRepo.RepoPath()) if err != nil { log.Error("OpenRepository[%s]: %v", pr.GetGitRefName(), err) @@ -132,7 +132,7 @@ func ToAPIPullRequest(ctx context.Context, pr *models.PullRequest, doer *user_mo apiPullRequest.Head.Name = "" } - if pr.HeadRepo != nil && pr.Flow == models.PullRequestFlowGithub { + if pr.HeadRepo != nil && pr.Flow == issues_model.PullRequestFlowGithub { p, err := access_model.GetUserRepoPermission(ctx, pr.HeadRepo, doer) if err != nil { log.Error("GetUserRepoPermission[%d]: %v", pr.HeadRepoID, err) diff --git a/modules/convert/pull_review.go b/modules/convert/pull_review.go index 907ccafb66..93ce208224 100644 --- a/modules/convert/pull_review.go +++ b/modules/convert/pull_review.go @@ -8,13 +8,13 @@ import ( "context" "strings" - "code.gitea.io/gitea/models" + issues_model "code.gitea.io/gitea/models/issues" user_model "code.gitea.io/gitea/models/user" api "code.gitea.io/gitea/modules/structs" ) // ToPullReview convert a review to api format -func ToPullReview(ctx context.Context, r *models.Review, doer *user_model.User) (*api.PullReview, error) { +func ToPullReview(ctx context.Context, r *issues_model.Review, doer *user_model.User) (*api.PullReview, error) { if err := r.LoadAttributes(ctx); err != nil { if !user_model.IsErrUserNotExist(err) { return nil, err @@ -44,15 +44,15 @@ func ToPullReview(ctx context.Context, r *models.Review, doer *user_model.User) } switch r.Type { - case models.ReviewTypeApprove: + case issues_model.ReviewTypeApprove: result.State = api.ReviewStateApproved - case models.ReviewTypeReject: + case issues_model.ReviewTypeReject: result.State = api.ReviewStateRequestChanges - case models.ReviewTypeComment: + case issues_model.ReviewTypeComment: result.State = api.ReviewStateComment - case models.ReviewTypePending: + case issues_model.ReviewTypePending: result.State = api.ReviewStatePending - case models.ReviewTypeRequest: + case issues_model.ReviewTypeRequest: result.State = api.ReviewStateRequestReview } @@ -60,11 +60,11 @@ func ToPullReview(ctx context.Context, r *models.Review, doer *user_model.User) } // ToPullReviewList convert a list of review to it's api format -func ToPullReviewList(ctx context.Context, rl []*models.Review, doer *user_model.User) ([]*api.PullReview, error) { +func ToPullReviewList(ctx context.Context, rl []*issues_model.Review, doer *user_model.User) ([]*api.PullReview, error) { result := make([]*api.PullReview, 0, len(rl)) for i := range rl { // show pending reviews only for the user who created them - if rl[i].Type == models.ReviewTypePending && !(doer.IsAdmin || doer.ID == rl[i].ReviewerID) { + if rl[i].Type == issues_model.ReviewTypePending && !(doer.IsAdmin || doer.ID == rl[i].ReviewerID) { continue } r, err := ToPullReview(ctx, rl[i], doer) @@ -77,7 +77,7 @@ func ToPullReviewList(ctx context.Context, rl []*models.Review, doer *user_model } // ToPullReviewCommentList convert the CodeComments of an review to it's api format -func ToPullReviewCommentList(ctx context.Context, review *models.Review, doer *user_model.User) ([]*api.PullReviewComment, error) { +func ToPullReviewCommentList(ctx context.Context, review *issues_model.Review, doer *user_model.User) ([]*api.PullReviewComment, error) { if err := review.LoadAttributes(ctx); err != nil { if !user_model.IsErrUserNotExist(err) { return nil, err diff --git a/modules/convert/pull_test.go b/modules/convert/pull_test.go index 8574ccfd26..10ef311399 100644 --- a/modules/convert/pull_test.go +++ b/modules/convert/pull_test.go @@ -7,7 +7,7 @@ package convert import ( "testing" - "code.gitea.io/gitea/models" + issues_model "code.gitea.io/gitea/models/issues" "code.gitea.io/gitea/models/perm" repo_model "code.gitea.io/gitea/models/repo" "code.gitea.io/gitea/models/unittest" @@ -21,7 +21,7 @@ func TestPullRequest_APIFormat(t *testing.T) { // with HeadRepo assert.NoError(t, unittest.PrepareTestDatabase()) headRepo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 1}).(*repo_model.Repository) - pr := unittest.AssertExistsAndLoadBean(t, &models.PullRequest{ID: 1}).(*models.PullRequest) + pr := unittest.AssertExistsAndLoadBean(t, &issues_model.PullRequest{ID: 1}).(*issues_model.PullRequest) assert.NoError(t, pr.LoadAttributes()) assert.NoError(t, pr.LoadIssue()) apiPullRequest := ToAPIPullRequest(git.DefaultContext, pr, nil) @@ -35,7 +35,7 @@ func TestPullRequest_APIFormat(t *testing.T) { }, apiPullRequest.Head) // withOut HeadRepo - pr = unittest.AssertExistsAndLoadBean(t, &models.PullRequest{ID: 1}).(*models.PullRequest) + pr = unittest.AssertExistsAndLoadBean(t, &issues_model.PullRequest{ID: 1}).(*issues_model.PullRequest) assert.NoError(t, pr.LoadIssue()) assert.NoError(t, pr.LoadAttributes()) // simulate fork deletion diff --git a/modules/doctor/dbconsistency.go b/modules/doctor/dbconsistency.go index 18cb2cdac6..b23807cca2 100644 --- a/modules/doctor/dbconsistency.go +++ b/modules/doctor/dbconsistency.go @@ -9,6 +9,7 @@ import ( "code.gitea.io/gitea/models" "code.gitea.io/gitea/models/db" + issues_model "code.gitea.io/gitea/models/issues" "code.gitea.io/gitea/models/migrations" repo_model "code.gitea.io/gitea/models/repo" "code.gitea.io/gitea/modules/log" @@ -64,10 +65,10 @@ func genericOrphanCheck(name, subject, refobject, joincond string) consistencyCh return consistencyCheck{ Name: name, Counter: func() (int64, error) { - return models.CountOrphanedObjects(subject, refobject, joincond) + return db.CountOrphanedObjects(subject, refobject, joincond) }, Fixer: func() (int64, error) { - err := models.DeleteOrphanedObjects(subject, refobject, joincond) + err := db.DeleteOrphanedObjects(subject, refobject, joincond) return -1, err }, } @@ -84,20 +85,20 @@ func checkDBConsistency(ctx context.Context, logger log.Logger, autofix bool) er { // find labels without existing repo or org Name: "Orphaned Labels without existing repository or organisation", - Counter: models.CountOrphanedLabels, - Fixer: asFixer(models.DeleteOrphanedLabels), + Counter: issues_model.CountOrphanedLabels, + Fixer: asFixer(issues_model.DeleteOrphanedLabels), }, { // find IssueLabels without existing label Name: "Orphaned Issue Labels without existing label", - Counter: models.CountOrphanedIssueLabels, - Fixer: asFixer(models.DeleteOrphanedIssueLabels), + Counter: issues_model.CountOrphanedIssueLabels, + Fixer: asFixer(issues_model.DeleteOrphanedIssueLabels), }, { // find issues without existing repository Name: "Orphaned Issues without existing repository", - Counter: models.CountOrphanedIssues, - Fixer: asFixer(models.DeleteOrphanedIssues), + Counter: issues_model.CountOrphanedIssues, + Fixer: asFixer(issues_model.DeleteOrphanedIssues), }, // find releases without existing repository genericOrphanCheck("Orphaned Releases without existing repository", @@ -127,22 +128,22 @@ func checkDBConsistency(ctx context.Context, logger log.Logger, autofix bool) er // find label comments with empty labels { Name: "Label comments with empty labels", - Counter: models.CountCommentTypeLabelWithEmptyLabel, - Fixer: models.FixCommentTypeLabelWithEmptyLabel, + Counter: issues_model.CountCommentTypeLabelWithEmptyLabel, + Fixer: issues_model.FixCommentTypeLabelWithEmptyLabel, FixedMessage: "Fixed", }, // find label comments with labels from outside the repository { Name: "Label comments with labels from outside the repository", - Counter: models.CountCommentTypeLabelWithOutsideLabels, - Fixer: models.FixCommentTypeLabelWithOutsideLabels, + Counter: issues_model.CountCommentTypeLabelWithOutsideLabels, + Fixer: issues_model.FixCommentTypeLabelWithOutsideLabels, FixedMessage: "Removed", }, // find issue_label with labels from outside the repository { Name: "IssueLabels with Labels from outside the repository", - Counter: models.CountIssueLabelWithOutsideLabels, - Fixer: models.FixIssueLabelWithOutsideLabels, + Counter: issues_model.CountIssueLabelWithOutsideLabels, + Fixer: issues_model.FixIssueLabelWithOutsideLabels, FixedMessage: "Removed", }, { diff --git a/modules/doctor/mergebase.go b/modules/doctor/mergebase.go index 61ee9e212b..46369290a1 100644 --- a/modules/doctor/mergebase.go +++ b/modules/doctor/mergebase.go @@ -9,8 +9,8 @@ import ( "fmt" "strings" - "code.gitea.io/gitea/models" "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/modules/git" "code.gitea.io/gitea/modules/log" @@ -18,13 +18,13 @@ import ( "xorm.io/builder" ) -func iteratePRs(ctx context.Context, repo *repo_model.Repository, each func(*repo_model.Repository, *models.PullRequest) error) error { +func iteratePRs(ctx context.Context, repo *repo_model.Repository, each func(*repo_model.Repository, *issues_model.PullRequest) error) error { return db.Iterate( ctx, - new(models.PullRequest), + new(issues_model.PullRequest), builder.Eq{"base_repo_id": repo.ID}, func(idx int, bean interface{}) error { - return each(repo, bean.(*models.PullRequest)) + return each(repo, bean.(*issues_model.PullRequest)) }, ) } @@ -35,7 +35,7 @@ func checkPRMergeBase(ctx context.Context, logger log.Logger, autofix bool) erro numPRsUpdated := 0 err := iterateRepositories(ctx, func(repo *repo_model.Repository) error { numRepos++ - return iteratePRs(ctx, repo, func(repo *repo_model.Repository, pr *models.PullRequest) error { + return iteratePRs(ctx, repo, func(repo *repo_model.Repository, pr *issues_model.PullRequest) error { numPRs++ pr.BaseRepo = repo repoPath := repo.RepoPath() diff --git a/modules/eventsource/manager_run.go b/modules/eventsource/manager_run.go index 127979ad63..6055cf7232 100644 --- a/modules/eventsource/manager_run.go +++ b/modules/eventsource/manager_run.go @@ -9,6 +9,7 @@ import ( "time" "code.gitea.io/gitea/models" + issues_model "code.gitea.io/gitea/models/issues" "code.gitea.io/gitea/modules/convert" "code.gitea.io/gitea/modules/graceful" "code.gitea.io/gitea/modules/json" @@ -84,7 +85,7 @@ loop: then = now if setting.Service.EnableTimetracking { - usersStopwatches, err := models.GetUIDsAndStopwatch() + usersStopwatches, err := issues_model.GetUIDsAndStopwatch() if err != nil { log.Error("Unable to get GetUIDsAndStopwatch: %v", err) return diff --git a/modules/indexer/issues/db.go b/modules/indexer/issues/db.go index e2badf64f2..d21c86337e 100644 --- a/modules/indexer/issues/db.go +++ b/modules/indexer/issues/db.go @@ -7,8 +7,8 @@ package issues import ( "context" - "code.gitea.io/gitea/models" "code.gitea.io/gitea/models/db" + issues_model "code.gitea.io/gitea/models/issues" ) // DBIndexer implements Indexer interface to use database's like search @@ -44,7 +44,7 @@ func (i *DBIndexer) Close() { // Search dummy function func (i *DBIndexer) Search(ctx context.Context, kw string, repoIDs []int64, limit, start int) (*SearchResult, error) { - total, ids, err := models.SearchIssueIDsByKeyword(ctx, kw, repoIDs, limit, start) + total, ids, err := issues_model.SearchIssueIDsByKeyword(ctx, kw, repoIDs, limit, start) if err != nil { return nil, err } diff --git a/modules/indexer/issues/indexer.go b/modules/indexer/issues/indexer.go index 85de4c75b3..da6a200aef 100644 --- a/modules/indexer/issues/indexer.go +++ b/modules/indexer/issues/indexer.go @@ -12,8 +12,8 @@ import ( "sync" "time" - "code.gitea.io/gitea/models" "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/modules/graceful" "code.gitea.io/gitea/modules/log" @@ -320,7 +320,7 @@ func populateIssueIndexer(ctx context.Context) { // UpdateRepoIndexer add/update all issues of the repositories func UpdateRepoIndexer(repo *repo_model.Repository) { - is, err := models.Issues(&models.IssuesOptions{ + is, err := issues_model.Issues(&issues_model.IssuesOptions{ RepoID: repo.ID, IsClosed: util.OptionalBoolNone, IsPull: util.OptionalBoolNone, @@ -329,7 +329,7 @@ func UpdateRepoIndexer(repo *repo_model.Repository) { log.Error("Issues: %v", err) return } - if err = models.IssueList(is).LoadDiscussComments(); err != nil { + if err = issues_model.IssueList(is).LoadDiscussComments(); err != nil { log.Error("LoadComments: %v", err) return } @@ -339,10 +339,10 @@ func UpdateRepoIndexer(repo *repo_model.Repository) { } // UpdateIssueIndexer add/update an issue to the issue indexer -func UpdateIssueIndexer(issue *models.Issue) { +func UpdateIssueIndexer(issue *issues_model.Issue) { var comments []string for _, comment := range issue.Comments { - if comment.Type == models.CommentTypeComment { + if comment.Type == issues_model.CommentTypeComment { comments = append(comments, comment.Content) } } @@ -362,7 +362,7 @@ func UpdateIssueIndexer(issue *models.Issue) { // DeleteRepoIssueIndexer deletes repo's all issues indexes func DeleteRepoIssueIndexer(repo *repo_model.Repository) { var ids []int64 - ids, err := models.GetIssueIDsByRepoID(db.DefaultContext, repo.ID) + ids, err := issues_model.GetIssueIDsByRepoID(db.DefaultContext, repo.ID) if err != nil { log.Error("getIssueIDsByRepoID failed: %v", err) return diff --git a/modules/notification/action/action.go b/modules/notification/action/action.go index 547498a9dc..e438f41485 100644 --- a/modules/notification/action/action.go +++ b/modules/notification/action/action.go @@ -11,6 +11,7 @@ import ( "code.gitea.io/gitea/models" "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/graceful" @@ -33,7 +34,7 @@ func NewNotifier() base.Notifier { return &actionNotifier{} } -func (a *actionNotifier) NotifyNewIssue(issue *models.Issue, mentions []*user_model.User) { +func (a *actionNotifier) NotifyNewIssue(issue *issues_model.Issue, mentions []*user_model.User) { if err := issue.LoadPoster(); err != nil { log.Error("issue.LoadPoster: %v", err) return @@ -58,7 +59,7 @@ func (a *actionNotifier) NotifyNewIssue(issue *models.Issue, mentions []*user_mo } // NotifyIssueChangeStatus notifies close or reopen issue to notifiers -func (a *actionNotifier) NotifyIssueChangeStatus(doer *user_model.User, issue *models.Issue, actionComment *models.Comment, closeOrReopen bool) { +func (a *actionNotifier) NotifyIssueChangeStatus(doer *user_model.User, issue *issues_model.Issue, actionComment *issues_model.Comment, closeOrReopen bool) { // Compose comment action, could be plain comment, close or reopen issue/pull request. // This object will be used to notify watchers in the end of function. act := &models.Action{ @@ -92,7 +93,7 @@ func (a *actionNotifier) NotifyIssueChangeStatus(doer *user_model.User, issue *m // NotifyCreateIssueComment notifies comment on an issue to notifiers func (a *actionNotifier) NotifyCreateIssueComment(doer *user_model.User, repo *repo_model.Repository, - issue *models.Issue, comment *models.Comment, mentions []*user_model.User, + issue *issues_model.Issue, comment *issues_model.Comment, mentions []*user_model.User, ) { act := &models.Action{ ActUserID: doer.ID, @@ -126,7 +127,7 @@ func (a *actionNotifier) NotifyCreateIssueComment(doer *user_model.User, repo *r } } -func (a *actionNotifier) NotifyNewPullRequest(pull *models.PullRequest, mentions []*user_model.User) { +func (a *actionNotifier) NotifyNewPullRequest(pull *issues_model.PullRequest, mentions []*user_model.User) { if err := pull.LoadIssue(); err != nil { log.Error("pull.LoadIssue: %v", err) return @@ -207,7 +208,7 @@ func (a *actionNotifier) NotifyForkRepository(doer *user_model.User, oldRepo, re } } -func (a *actionNotifier) NotifyPullRequestReview(pr *models.PullRequest, review *models.Review, comment *models.Comment, mentions []*user_model.User) { +func (a *actionNotifier) NotifyPullRequestReview(pr *issues_model.PullRequest, review *issues_model.Review, comment *issues_model.Comment, mentions []*user_model.User) { ctx, _, finished := process.GetManager().AddContext(graceful.GetManager().HammerContext(), fmt.Sprintf("actionNotifier.NotifyPullRequestReview Pull[%d] #%d in [%d]", pr.ID, pr.Index, pr.BaseRepoID)) defer finished() @@ -239,7 +240,7 @@ func (a *actionNotifier) NotifyPullRequestReview(pr *models.PullRequest, review } } - if review.Type != models.ReviewTypeComment || strings.TrimSpace(comment.Content) != "" { + if review.Type != issues_model.ReviewTypeComment || strings.TrimSpace(comment.Content) != "" { action := &models.Action{ ActUserID: review.Reviewer.ID, ActUser: review.Reviewer, @@ -252,9 +253,9 @@ func (a *actionNotifier) NotifyPullRequestReview(pr *models.PullRequest, review } switch review.Type { - case models.ReviewTypeApprove: + case issues_model.ReviewTypeApprove: action.OpType = models.ActionApprovePullRequest - case models.ReviewTypeReject: + case issues_model.ReviewTypeReject: action.OpType = models.ActionRejectPullRequest default: action.OpType = models.ActionCommentPull @@ -268,7 +269,7 @@ func (a *actionNotifier) NotifyPullRequestReview(pr *models.PullRequest, review } } -func (*actionNotifier) NotifyMergePullRequest(pr *models.PullRequest, doer *user_model.User) { +func (*actionNotifier) NotifyMergePullRequest(pr *issues_model.PullRequest, doer *user_model.User) { if err := models.NotifyWatchers(&models.Action{ ActUserID: doer.ID, ActUser: doer, @@ -282,7 +283,7 @@ func (*actionNotifier) NotifyMergePullRequest(pr *models.PullRequest, doer *user } } -func (*actionNotifier) NotifyPullRevieweDismiss(doer *user_model.User, review *models.Review, comment *models.Comment) { +func (*actionNotifier) NotifyPullRevieweDismiss(doer *user_model.User, review *issues_model.Review, comment *issues_model.Comment) { reviewerName := review.Reviewer.Name if len(review.OriginalAuthor) > 0 { reviewerName = review.OriginalAuthor diff --git a/modules/notification/base/notifier.go b/modules/notification/base/notifier.go index 2b8be18ad3..31fa8f5f18 100644 --- a/modules/notification/base/notifier.go +++ b/modules/notification/base/notifier.go @@ -6,6 +6,7 @@ package base import ( "code.gitea.io/gitea/models" + issues_model "code.gitea.io/gitea/models/issues" packages_model "code.gitea.io/gitea/models/packages" repo_model "code.gitea.io/gitea/models/repo" user_model "code.gitea.io/gitea/models/user" @@ -21,30 +22,30 @@ type Notifier interface { NotifyForkRepository(doer *user_model.User, oldRepo, repo *repo_model.Repository) NotifyRenameRepository(doer *user_model.User, repo *repo_model.Repository, oldRepoName string) NotifyTransferRepository(doer *user_model.User, repo *repo_model.Repository, oldOwnerName string) - NotifyNewIssue(issue *models.Issue, mentions []*user_model.User) - NotifyIssueChangeStatus(*user_model.User, *models.Issue, *models.Comment, bool) - NotifyDeleteIssue(*user_model.User, *models.Issue) - NotifyIssueChangeMilestone(doer *user_model.User, issue *models.Issue, oldMilestoneID int64) - NotifyIssueChangeAssignee(doer *user_model.User, issue *models.Issue, assignee *user_model.User, removed bool, comment *models.Comment) - NotifyPullReviewRequest(doer *user_model.User, issue *models.Issue, reviewer *user_model.User, isRequest bool, comment *models.Comment) - NotifyIssueChangeContent(doer *user_model.User, issue *models.Issue, oldContent string) - NotifyIssueClearLabels(doer *user_model.User, issue *models.Issue) - NotifyIssueChangeTitle(doer *user_model.User, issue *models.Issue, oldTitle string) - NotifyIssueChangeRef(doer *user_model.User, issue *models.Issue, oldRef string) - NotifyIssueChangeLabels(doer *user_model.User, issue *models.Issue, - addedLabels, removedLabels []*models.Label) - NotifyNewPullRequest(pr *models.PullRequest, mentions []*user_model.User) - NotifyMergePullRequest(*models.PullRequest, *user_model.User) - NotifyPullRequestSynchronized(doer *user_model.User, pr *models.PullRequest) - NotifyPullRequestReview(pr *models.PullRequest, review *models.Review, comment *models.Comment, mentions []*user_model.User) - NotifyPullRequestCodeComment(pr *models.PullRequest, comment *models.Comment, mentions []*user_model.User) - NotifyPullRequestChangeTargetBranch(doer *user_model.User, pr *models.PullRequest, oldBranch string) - NotifyPullRequestPushCommits(doer *user_model.User, pr *models.PullRequest, comment *models.Comment) - NotifyPullRevieweDismiss(doer *user_model.User, review *models.Review, comment *models.Comment) + NotifyNewIssue(issue *issues_model.Issue, mentions []*user_model.User) + NotifyIssueChangeStatus(*user_model.User, *issues_model.Issue, *issues_model.Comment, bool) + NotifyDeleteIssue(*user_model.User, *issues_model.Issue) + NotifyIssueChangeMilestone(doer *user_model.User, issue *issues_model.Issue, oldMilestoneID int64) + NotifyIssueChangeAssignee(doer *user_model.User, issue *issues_model.Issue, assignee *user_model.User, removed bool, comment *issues_model.Comment) + NotifyPullReviewRequest(doer *user_model.User, issue *issues_model.Issue, reviewer *user_model.User, isRequest bool, comment *issues_model.Comment) + NotifyIssueChangeContent(doer *user_model.User, issue *issues_model.Issue, oldContent string) + NotifyIssueClearLabels(doer *user_model.User, issue *issues_model.Issue) + NotifyIssueChangeTitle(doer *user_model.User, issue *issues_model.Issue, oldTitle string) + NotifyIssueChangeRef(doer *user_model.User, issue *issues_model.Issue, oldRef string) + NotifyIssueChangeLabels(doer *user_model.User, issue *issues_model.Issue, + addedLabels, removedLabels []*issues_model.Label) + NotifyNewPullRequest(pr *issues_model.PullRequest, mentions []*user_model.User) + NotifyMergePullRequest(*issues_model.PullRequest, *user_model.User) + NotifyPullRequestSynchronized(doer *user_model.User, pr *issues_model.PullRequest) + NotifyPullRequestReview(pr *issues_model.PullRequest, review *issues_model.Review, comment *issues_model.Comment, mentions []*user_model.User) + NotifyPullRequestCodeComment(pr *issues_model.PullRequest, comment *issues_model.Comment, mentions []*user_model.User) + NotifyPullRequestChangeTargetBranch(doer *user_model.User, pr *issues_model.PullRequest, oldBranch string) + NotifyPullRequestPushCommits(doer *user_model.User, pr *issues_model.PullRequest, comment *issues_model.Comment) + NotifyPullRevieweDismiss(doer *user_model.User, review *issues_model.Review, comment *issues_model.Comment) NotifyCreateIssueComment(doer *user_model.User, repo *repo_model.Repository, - issue *models.Issue, comment *models.Comment, mentions []*user_model.User) - NotifyUpdateComment(*user_model.User, *models.Comment, string) - NotifyDeleteComment(*user_model.User, *models.Comment) + issue *issues_model.Issue, comment *issues_model.Comment, mentions []*user_model.User) + NotifyUpdateComment(*user_model.User, *issues_model.Comment, string) + NotifyDeleteComment(*user_model.User, *issues_model.Comment) NotifyNewRelease(rel *models.Release) NotifyUpdateRelease(doer *user_model.User, rel *models.Release) NotifyDeleteRelease(doer *user_model.User, rel *models.Release) diff --git a/modules/notification/base/null.go b/modules/notification/base/null.go index 29b5f0c97e..d336f09301 100644 --- a/modules/notification/base/null.go +++ b/modules/notification/base/null.go @@ -6,6 +6,7 @@ package base import ( "code.gitea.io/gitea/models" + issues_model "code.gitea.io/gitea/models/issues" packages_model "code.gitea.io/gitea/models/packages" repo_model "code.gitea.io/gitea/models/repo" user_model "code.gitea.io/gitea/models/user" @@ -23,59 +24,59 @@ func (*NullNotifier) Run() { // NotifyCreateIssueComment places a place holder function func (*NullNotifier) NotifyCreateIssueComment(doer *user_model.User, repo *repo_model.Repository, - issue *models.Issue, comment *models.Comment, mentions []*user_model.User) { + issue *issues_model.Issue, comment *issues_model.Comment, mentions []*user_model.User) { } // NotifyNewIssue places a place holder function -func (*NullNotifier) NotifyNewIssue(issue *models.Issue, mentions []*user_model.User) { +func (*NullNotifier) NotifyNewIssue(issue *issues_model.Issue, mentions []*user_model.User) { } // NotifyIssueChangeStatus places a place holder function -func (*NullNotifier) NotifyIssueChangeStatus(doer *user_model.User, issue *models.Issue, actionComment *models.Comment, isClosed bool) { +func (*NullNotifier) NotifyIssueChangeStatus(doer *user_model.User, issue *issues_model.Issue, actionComment *issues_model.Comment, isClosed bool) { } // NotifyDeleteIssue notify when some issue deleted -func (*NullNotifier) NotifyDeleteIssue(doer *user_model.User, issue *models.Issue) { +func (*NullNotifier) NotifyDeleteIssue(doer *user_model.User, issue *issues_model.Issue) { } // NotifyNewPullRequest places a place holder function -func (*NullNotifier) NotifyNewPullRequest(pr *models.PullRequest, mentions []*user_model.User) { +func (*NullNotifier) NotifyNewPullRequest(pr *issues_model.PullRequest, mentions []*user_model.User) { } // NotifyPullRequestReview places a place holder function -func (*NullNotifier) NotifyPullRequestReview(pr *models.PullRequest, r *models.Review, comment *models.Comment, mentions []*user_model.User) { +func (*NullNotifier) NotifyPullRequestReview(pr *issues_model.PullRequest, r *issues_model.Review, comment *issues_model.Comment, mentions []*user_model.User) { } // NotifyPullRequestCodeComment places a place holder function -func (*NullNotifier) NotifyPullRequestCodeComment(pr *models.PullRequest, comment *models.Comment, mentions []*user_model.User) { +func (*NullNotifier) NotifyPullRequestCodeComment(pr *issues_model.PullRequest, comment *issues_model.Comment, mentions []*user_model.User) { } // NotifyMergePullRequest places a place holder function -func (*NullNotifier) NotifyMergePullRequest(pr *models.PullRequest, doer *user_model.User) { +func (*NullNotifier) NotifyMergePullRequest(pr *issues_model.PullRequest, doer *user_model.User) { } // NotifyPullRequestSynchronized places a place holder function -func (*NullNotifier) NotifyPullRequestSynchronized(doer *user_model.User, pr *models.PullRequest) { +func (*NullNotifier) NotifyPullRequestSynchronized(doer *user_model.User, pr *issues_model.PullRequest) { } // NotifyPullRequestChangeTargetBranch places a place holder function -func (*NullNotifier) NotifyPullRequestChangeTargetBranch(doer *user_model.User, pr *models.PullRequest, oldBranch string) { +func (*NullNotifier) NotifyPullRequestChangeTargetBranch(doer *user_model.User, pr *issues_model.PullRequest, oldBranch string) { } // NotifyPullRequestPushCommits notifies when push commits to pull request's head branch -func (*NullNotifier) NotifyPullRequestPushCommits(doer *user_model.User, pr *models.PullRequest, comment *models.Comment) { +func (*NullNotifier) NotifyPullRequestPushCommits(doer *user_model.User, pr *issues_model.PullRequest, comment *issues_model.Comment) { } // NotifyPullRevieweDismiss notifies when a review was dismissed by repo admin -func (*NullNotifier) NotifyPullRevieweDismiss(doer *user_model.User, review *models.Review, comment *models.Comment) { +func (*NullNotifier) NotifyPullRevieweDismiss(doer *user_model.User, review *issues_model.Review, comment *issues_model.Comment) { } // NotifyUpdateComment places a place holder function -func (*NullNotifier) NotifyUpdateComment(doer *user_model.User, c *models.Comment, oldContent string) { +func (*NullNotifier) NotifyUpdateComment(doer *user_model.User, c *issues_model.Comment, oldContent string) { } // NotifyDeleteComment places a place holder function -func (*NullNotifier) NotifyDeleteComment(doer *user_model.User, c *models.Comment) { +func (*NullNotifier) NotifyDeleteComment(doer *user_model.User, c *issues_model.Comment) { } // NotifyNewRelease places a place holder function @@ -91,36 +92,36 @@ func (*NullNotifier) NotifyDeleteRelease(doer *user_model.User, rel *models.Rele } // NotifyIssueChangeMilestone places a place holder function -func (*NullNotifier) NotifyIssueChangeMilestone(doer *user_model.User, issue *models.Issue, oldMilestoneID int64) { +func (*NullNotifier) NotifyIssueChangeMilestone(doer *user_model.User, issue *issues_model.Issue, oldMilestoneID int64) { } // NotifyIssueChangeContent places a place holder function -func (*NullNotifier) NotifyIssueChangeContent(doer *user_model.User, issue *models.Issue, oldContent string) { +func (*NullNotifier) NotifyIssueChangeContent(doer *user_model.User, issue *issues_model.Issue, oldContent string) { } // NotifyIssueChangeAssignee places a place holder function -func (*NullNotifier) NotifyIssueChangeAssignee(doer *user_model.User, issue *models.Issue, assignee *user_model.User, removed bool, comment *models.Comment) { +func (*NullNotifier) NotifyIssueChangeAssignee(doer *user_model.User, issue *issues_model.Issue, assignee *user_model.User, removed bool, comment *issues_model.Comment) { } // NotifyPullReviewRequest places a place holder function -func (*NullNotifier) NotifyPullReviewRequest(doer *user_model.User, issue *models.Issue, reviewer *user_model.User, isRequest bool, comment *models.Comment) { +func (*NullNotifier) NotifyPullReviewRequest(doer *user_model.User, issue *issues_model.Issue, reviewer *user_model.User, isRequest bool, comment *issues_model.Comment) { } // NotifyIssueClearLabels places a place holder function -func (*NullNotifier) NotifyIssueClearLabels(doer *user_model.User, issue *models.Issue) { +func (*NullNotifier) NotifyIssueClearLabels(doer *user_model.User, issue *issues_model.Issue) { } // NotifyIssueChangeTitle places a place holder function -func (*NullNotifier) NotifyIssueChangeTitle(doer *user_model.User, issue *models.Issue, oldTitle string) { +func (*NullNotifier) NotifyIssueChangeTitle(doer *user_model.User, issue *issues_model.Issue, oldTitle string) { } // NotifyIssueChangeRef places a place holder function -func (*NullNotifier) NotifyIssueChangeRef(doer *user_model.User, issue *models.Issue, oldTitle string) { +func (*NullNotifier) NotifyIssueChangeRef(doer *user_model.User, issue *issues_model.Issue, oldTitle string) { } // NotifyIssueChangeLabels places a place holder function -func (*NullNotifier) NotifyIssueChangeLabels(doer *user_model.User, issue *models.Issue, - addedLabels, removedLabels []*models.Label) { +func (*NullNotifier) NotifyIssueChangeLabels(doer *user_model.User, issue *issues_model.Issue, + addedLabels, removedLabels []*issues_model.Label) { } // NotifyCreateRepository places a place holder function diff --git a/modules/notification/indexer/indexer.go b/modules/notification/indexer/indexer.go index 48a491f3f1..fc9afdd4bc 100644 --- a/modules/notification/indexer/indexer.go +++ b/modules/notification/indexer/indexer.go @@ -5,7 +5,7 @@ package indexer import ( - "code.gitea.io/gitea/models" + 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" @@ -30,9 +30,9 @@ func NewNotifier() base.Notifier { } func (r *indexerNotifier) NotifyCreateIssueComment(doer *user_model.User, repo *repo_model.Repository, - issue *models.Issue, comment *models.Comment, mentions []*user_model.User, + issue *issues_model.Issue, comment *issues_model.Comment, mentions []*user_model.User, ) { - if comment.Type == models.CommentTypeComment { + if comment.Type == issues_model.CommentTypeComment { if issue.Comments == nil { if err := issue.LoadDiscussComments(); err != nil { log.Error("LoadComments failed: %v", err) @@ -46,16 +46,16 @@ func (r *indexerNotifier) NotifyCreateIssueComment(doer *user_model.User, repo * } } -func (r *indexerNotifier) NotifyNewIssue(issue *models.Issue, mentions []*user_model.User) { +func (r *indexerNotifier) NotifyNewIssue(issue *issues_model.Issue, mentions []*user_model.User) { issue_indexer.UpdateIssueIndexer(issue) } -func (r *indexerNotifier) NotifyNewPullRequest(pr *models.PullRequest, mentions []*user_model.User) { +func (r *indexerNotifier) NotifyNewPullRequest(pr *issues_model.PullRequest, mentions []*user_model.User) { issue_indexer.UpdateIssueIndexer(pr.Issue) } -func (r *indexerNotifier) NotifyUpdateComment(doer *user_model.User, c *models.Comment, oldContent string) { - if c.Type == models.CommentTypeComment { +func (r *indexerNotifier) NotifyUpdateComment(doer *user_model.User, c *issues_model.Comment, oldContent string) { + if c.Type == issues_model.CommentTypeComment { var found bool if c.Issue.Comments != nil { for i := 0; i < len(c.Issue.Comments); i++ { @@ -78,8 +78,8 @@ func (r *indexerNotifier) NotifyUpdateComment(doer *user_model.User, c *models.C } } -func (r *indexerNotifier) NotifyDeleteComment(doer *user_model.User, comment *models.Comment) { - if comment.Type == models.CommentTypeComment { +func (r *indexerNotifier) NotifyDeleteComment(doer *user_model.User, comment *issues_model.Comment) { + if comment.Type == issues_model.CommentTypeComment { if err := comment.LoadIssue(); err != nil { log.Error("LoadIssue: %v", err) return @@ -142,14 +142,14 @@ func (r *indexerNotifier) NotifySyncPushCommits(pusher *user_model.User, repo *r } } -func (r *indexerNotifier) NotifyIssueChangeContent(doer *user_model.User, issue *models.Issue, oldContent string) { +func (r *indexerNotifier) NotifyIssueChangeContent(doer *user_model.User, issue *issues_model.Issue, oldContent string) { issue_indexer.UpdateIssueIndexer(issue) } -func (r *indexerNotifier) NotifyIssueChangeTitle(doer *user_model.User, issue *models.Issue, oldTitle string) { +func (r *indexerNotifier) NotifyIssueChangeTitle(doer *user_model.User, issue *issues_model.Issue, oldTitle string) { issue_indexer.UpdateIssueIndexer(issue) } -func (r *indexerNotifier) NotifyIssueChangeRef(doer *user_model.User, issue *models.Issue, oldRef string) { +func (r *indexerNotifier) NotifyIssueChangeRef(doer *user_model.User, issue *issues_model.Issue, oldRef string) { issue_indexer.UpdateIssueIndexer(issue) } diff --git a/modules/notification/mail/mail.go b/modules/notification/mail/mail.go index 138e438751..1f217304b0 100644 --- a/modules/notification/mail/mail.go +++ b/modules/notification/mail/mail.go @@ -8,6 +8,7 @@ import ( "fmt" "code.gitea.io/gitea/models" + 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/graceful" @@ -29,21 +30,21 @@ func NewNotifier() base.Notifier { } func (m *mailNotifier) NotifyCreateIssueComment(doer *user_model.User, repo *repo_model.Repository, - issue *models.Issue, comment *models.Comment, mentions []*user_model.User, + issue *issues_model.Issue, comment *issues_model.Comment, mentions []*user_model.User, ) { ctx, _, finished := process.GetManager().AddContext(graceful.GetManager().HammerContext(), fmt.Sprintf("mailNotifier.NotifyCreateIssueComment Issue[%d] #%d in [%d]", issue.ID, issue.Index, issue.RepoID)) defer finished() var act models.ActionType - if comment.Type == models.CommentTypeClose { + if comment.Type == issues_model.CommentTypeClose { act = models.ActionCloseIssue - } else if comment.Type == models.CommentTypeReopen { + } else if comment.Type == issues_model.CommentTypeReopen { act = models.ActionReopenIssue - } else if comment.Type == models.CommentTypeComment { + } else if comment.Type == issues_model.CommentTypeComment { act = models.ActionCommentIssue - } else if comment.Type == models.CommentTypeCode { + } else if comment.Type == issues_model.CommentTypeCode { act = models.ActionCommentIssue - } else if comment.Type == models.CommentTypePullRequestPush { + } else if comment.Type == issues_model.CommentTypePullRequestPush { act = 0 } @@ -52,13 +53,13 @@ func (m *mailNotifier) NotifyCreateIssueComment(doer *user_model.User, repo *rep } } -func (m *mailNotifier) NotifyNewIssue(issue *models.Issue, mentions []*user_model.User) { +func (m *mailNotifier) NotifyNewIssue(issue *issues_model.Issue, mentions []*user_model.User) { if err := mailer.MailParticipants(issue, issue.Poster, models.ActionCreateIssue, mentions); err != nil { log.Error("MailParticipants: %v", err) } } -func (m *mailNotifier) NotifyIssueChangeStatus(doer *user_model.User, issue *models.Issue, actionComment *models.Comment, isClosed bool) { +func (m *mailNotifier) NotifyIssueChangeStatus(doer *user_model.User, issue *issues_model.Issue, actionComment *issues_model.Comment, isClosed bool) { var actionType models.ActionType if issue.IsPull { if isClosed { @@ -79,34 +80,34 @@ func (m *mailNotifier) NotifyIssueChangeStatus(doer *user_model.User, issue *mod } } -func (m *mailNotifier) NotifyIssueChangeTitle(doer *user_model.User, issue *models.Issue, oldTitle string) { +func (m *mailNotifier) NotifyIssueChangeTitle(doer *user_model.User, issue *issues_model.Issue, oldTitle string) { if err := issue.LoadPullRequest(); err != nil { log.Error("issue.LoadPullRequest: %v", err) return } - if issue.IsPull && models.HasWorkInProgressPrefix(oldTitle) && !issue.PullRequest.IsWorkInProgress() { + if issue.IsPull && issues_model.HasWorkInProgressPrefix(oldTitle) && !issue.PullRequest.IsWorkInProgress() { if err := mailer.MailParticipants(issue, doer, models.ActionPullRequestReadyForReview, nil); err != nil { log.Error("MailParticipants: %v", err) } } } -func (m *mailNotifier) NotifyNewPullRequest(pr *models.PullRequest, mentions []*user_model.User) { +func (m *mailNotifier) NotifyNewPullRequest(pr *issues_model.PullRequest, mentions []*user_model.User) { if err := mailer.MailParticipants(pr.Issue, pr.Issue.Poster, models.ActionCreatePullRequest, mentions); err != nil { log.Error("MailParticipants: %v", err) } } -func (m *mailNotifier) NotifyPullRequestReview(pr *models.PullRequest, r *models.Review, comment *models.Comment, mentions []*user_model.User) { +func (m *mailNotifier) NotifyPullRequestReview(pr *issues_model.PullRequest, r *issues_model.Review, comment *issues_model.Comment, mentions []*user_model.User) { ctx, _, finished := process.GetManager().AddContext(graceful.GetManager().HammerContext(), fmt.Sprintf("mailNotifier.NotifyPullRequestReview Pull[%d] #%d in [%d]", pr.ID, pr.Index, pr.BaseRepoID)) defer finished() var act models.ActionType - if comment.Type == models.CommentTypeClose { + if comment.Type == issues_model.CommentTypeClose { act = models.ActionCloseIssue - } else if comment.Type == models.CommentTypeReopen { + } else if comment.Type == issues_model.CommentTypeReopen { act = models.ActionReopenIssue - } else if comment.Type == models.CommentTypeComment { + } else if comment.Type == issues_model.CommentTypeComment { act = models.ActionCommentPull } if err := mailer.MailParticipantsComment(ctx, comment, act, pr.Issue, mentions); err != nil { @@ -114,7 +115,7 @@ func (m *mailNotifier) NotifyPullRequestReview(pr *models.PullRequest, r *models } } -func (m *mailNotifier) NotifyPullRequestCodeComment(pr *models.PullRequest, comment *models.Comment, mentions []*user_model.User) { +func (m *mailNotifier) NotifyPullRequestCodeComment(pr *issues_model.PullRequest, comment *issues_model.Comment, mentions []*user_model.User) { ctx, _, finished := process.GetManager().AddContext(graceful.GetManager().HammerContext(), fmt.Sprintf("mailNotifier.NotifyPullRequestCodeComment Pull[%d] #%d in [%d]", pr.ID, pr.Index, pr.BaseRepoID)) defer finished() @@ -123,7 +124,7 @@ func (m *mailNotifier) NotifyPullRequestCodeComment(pr *models.PullRequest, comm } } -func (m *mailNotifier) NotifyIssueChangeAssignee(doer *user_model.User, issue *models.Issue, assignee *user_model.User, removed bool, comment *models.Comment) { +func (m *mailNotifier) NotifyIssueChangeAssignee(doer *user_model.User, issue *issues_model.Issue, assignee *user_model.User, removed bool, comment *issues_model.Comment) { // mail only sent to added assignees and not self-assignee if !removed && doer.ID != assignee.ID && (assignee.EmailNotifications() == user_model.EmailNotificationsEnabled || assignee.EmailNotifications() == user_model.EmailNotificationsOnMention) { ct := fmt.Sprintf("Assigned #%d.", issue.Index) @@ -133,7 +134,7 @@ func (m *mailNotifier) NotifyIssueChangeAssignee(doer *user_model.User, issue *m } } -func (m *mailNotifier) NotifyPullReviewRequest(doer *user_model.User, issue *models.Issue, reviewer *user_model.User, isRequest bool, comment *models.Comment) { +func (m *mailNotifier) NotifyPullReviewRequest(doer *user_model.User, issue *issues_model.Issue, reviewer *user_model.User, isRequest bool, comment *issues_model.Comment) { if isRequest && doer.ID != reviewer.ID && (reviewer.EmailNotifications() == user_model.EmailNotificationsEnabled || reviewer.EmailNotifications() == user_model.EmailNotificationsOnMention) { ct := fmt.Sprintf("Requested to review %s.", issue.HTMLURL()) if err := mailer.SendIssueAssignedMail(issue, doer, ct, comment, []*user_model.User{reviewer}); err != nil { @@ -142,7 +143,7 @@ func (m *mailNotifier) NotifyPullReviewRequest(doer *user_model.User, issue *mod } } -func (m *mailNotifier) NotifyMergePullRequest(pr *models.PullRequest, doer *user_model.User) { +func (m *mailNotifier) NotifyMergePullRequest(pr *issues_model.PullRequest, doer *user_model.User) { if err := pr.LoadIssue(); err != nil { log.Error("pr.LoadIssue: %v", err) return @@ -152,7 +153,7 @@ func (m *mailNotifier) NotifyMergePullRequest(pr *models.PullRequest, doer *user } } -func (m *mailNotifier) NotifyPullRequestPushCommits(doer *user_model.User, pr *models.PullRequest, comment *models.Comment) { +func (m *mailNotifier) NotifyPullRequestPushCommits(doer *user_model.User, pr *issues_model.PullRequest, comment *issues_model.Comment) { ctx, _, finished := process.GetManager().AddContext(graceful.GetManager().HammerContext(), fmt.Sprintf("mailNotifier.NotifyPullRequestPushCommits Pull[%d] #%d in [%d]", pr.ID, pr.Index, pr.BaseRepoID)) defer finished() @@ -179,7 +180,7 @@ func (m *mailNotifier) NotifyPullRequestPushCommits(doer *user_model.User, pr *m m.NotifyCreateIssueComment(doer, comment.Issue.Repo, comment.Issue, comment, nil) } -func (m *mailNotifier) NotifyPullRevieweDismiss(doer *user_model.User, review *models.Review, comment *models.Comment) { +func (m *mailNotifier) NotifyPullRevieweDismiss(doer *user_model.User, review *issues_model.Review, comment *issues_model.Comment) { ctx, _, finished := process.GetManager().AddContext(graceful.GetManager().HammerContext(), fmt.Sprintf("mailNotifier.NotifyPullRevieweDismiss Review[%d] in Issue[%d]", review.ID, review.IssueID)) defer finished() diff --git a/modules/notification/notification.go b/modules/notification/notification.go index 90ff87941f..d60a880bec 100644 --- a/modules/notification/notification.go +++ b/modules/notification/notification.go @@ -6,6 +6,7 @@ package notification import ( "code.gitea.io/gitea/models" + issues_model "code.gitea.io/gitea/models/issues" packages_model "code.gitea.io/gitea/models/packages" repo_model "code.gitea.io/gitea/models/repo" user_model "code.gitea.io/gitea/models/user" @@ -40,7 +41,7 @@ func NewContext() { // NotifyCreateIssueComment notifies issue comment related message to notifiers func NotifyCreateIssueComment(doer *user_model.User, repo *repo_model.Repository, - issue *models.Issue, comment *models.Comment, mentions []*user_model.User, + issue *issues_model.Issue, comment *issues_model.Comment, mentions []*user_model.User, ) { for _, notifier := range notifiers { notifier.NotifyCreateIssueComment(doer, repo, issue, comment, mentions) @@ -48,91 +49,91 @@ func NotifyCreateIssueComment(doer *user_model.User, repo *repo_model.Repository } // NotifyNewIssue notifies new issue to notifiers -func NotifyNewIssue(issue *models.Issue, mentions []*user_model.User) { +func NotifyNewIssue(issue *issues_model.Issue, mentions []*user_model.User) { for _, notifier := range notifiers { notifier.NotifyNewIssue(issue, mentions) } } // NotifyIssueChangeStatus notifies close or reopen issue to notifiers -func NotifyIssueChangeStatus(doer *user_model.User, issue *models.Issue, actionComment *models.Comment, closeOrReopen bool) { +func NotifyIssueChangeStatus(doer *user_model.User, issue *issues_model.Issue, actionComment *issues_model.Comment, closeOrReopen bool) { for _, notifier := range notifiers { notifier.NotifyIssueChangeStatus(doer, issue, actionComment, closeOrReopen) } } // NotifyDeleteIssue notify when some issue deleted -func NotifyDeleteIssue(doer *user_model.User, issue *models.Issue) { +func NotifyDeleteIssue(doer *user_model.User, issue *issues_model.Issue) { for _, notifier := range notifiers { notifier.NotifyDeleteIssue(doer, issue) } } // NotifyMergePullRequest notifies merge pull request to notifiers -func NotifyMergePullRequest(pr *models.PullRequest, doer *user_model.User) { +func NotifyMergePullRequest(pr *issues_model.PullRequest, doer *user_model.User) { for _, notifier := range notifiers { notifier.NotifyMergePullRequest(pr, doer) } } // NotifyNewPullRequest notifies new pull request to notifiers -func NotifyNewPullRequest(pr *models.PullRequest, mentions []*user_model.User) { +func NotifyNewPullRequest(pr *issues_model.PullRequest, mentions []*user_model.User) { for _, notifier := range notifiers { notifier.NotifyNewPullRequest(pr, mentions) } } // NotifyPullRequestSynchronized notifies Synchronized pull request -func NotifyPullRequestSynchronized(doer *user_model.User, pr *models.PullRequest) { +func NotifyPullRequestSynchronized(doer *user_model.User, pr *issues_model.PullRequest) { for _, notifier := range notifiers { notifier.NotifyPullRequestSynchronized(doer, pr) } } // NotifyPullRequestReview notifies new pull request review -func NotifyPullRequestReview(pr *models.PullRequest, review *models.Review, comment *models.Comment, mentions []*user_model.User) { +func NotifyPullRequestReview(pr *issues_model.PullRequest, review *issues_model.Review, comment *issues_model.Comment, mentions []*user_model.User) { for _, notifier := range notifiers { notifier.NotifyPullRequestReview(pr, review, comment, mentions) } } // NotifyPullRequestCodeComment notifies new pull request code comment -func NotifyPullRequestCodeComment(pr *models.PullRequest, comment *models.Comment, mentions []*user_model.User) { +func NotifyPullRequestCodeComment(pr *issues_model.PullRequest, comment *issues_model.Comment, mentions []*user_model.User) { for _, notifier := range notifiers { notifier.NotifyPullRequestCodeComment(pr, comment, mentions) } } // NotifyPullRequestChangeTargetBranch notifies when a pull request's target branch was changed -func NotifyPullRequestChangeTargetBranch(doer *user_model.User, pr *models.PullRequest, oldBranch string) { +func NotifyPullRequestChangeTargetBranch(doer *user_model.User, pr *issues_model.PullRequest, oldBranch string) { for _, notifier := range notifiers { notifier.NotifyPullRequestChangeTargetBranch(doer, pr, oldBranch) } } // NotifyPullRequestPushCommits notifies when push commits to pull request's head branch -func NotifyPullRequestPushCommits(doer *user_model.User, pr *models.PullRequest, comment *models.Comment) { +func NotifyPullRequestPushCommits(doer *user_model.User, pr *issues_model.PullRequest, comment *issues_model.Comment) { for _, notifier := range notifiers { notifier.NotifyPullRequestPushCommits(doer, pr, comment) } } // NotifyPullRevieweDismiss notifies when a review was dismissed by repo admin -func NotifyPullRevieweDismiss(doer *user_model.User, review *models.Review, comment *models.Comment) { +func NotifyPullRevieweDismiss(doer *user_model.User, review *issues_model.Review, comment *issues_model.Comment) { for _, notifier := range notifiers { notifier.NotifyPullRevieweDismiss(doer, review, comment) } } // NotifyUpdateComment notifies update comment to notifiers -func NotifyUpdateComment(doer *user_model.User, c *models.Comment, oldContent string) { +func NotifyUpdateComment(doer *user_model.User, c *issues_model.Comment, oldContent string) { for _, notifier := range notifiers { notifier.NotifyUpdateComment(doer, c, oldContent) } } // NotifyDeleteComment notifies delete comment to notifiers -func NotifyDeleteComment(doer *user_model.User, c *models.Comment) { +func NotifyDeleteComment(doer *user_model.User, c *issues_model.Comment) { for _, notifier := range notifiers { notifier.NotifyDeleteComment(doer, c) } @@ -160,57 +161,57 @@ func NotifyDeleteRelease(doer *user_model.User, rel *models.Release) { } // NotifyIssueChangeMilestone notifies change milestone to notifiers -func NotifyIssueChangeMilestone(doer *user_model.User, issue *models.Issue, oldMilestoneID int64) { +func NotifyIssueChangeMilestone(doer *user_model.User, issue *issues_model.Issue, oldMilestoneID int64) { for _, notifier := range notifiers { notifier.NotifyIssueChangeMilestone(doer, issue, oldMilestoneID) } } // NotifyIssueChangeContent notifies change content to notifiers -func NotifyIssueChangeContent(doer *user_model.User, issue *models.Issue, oldContent string) { +func NotifyIssueChangeContent(doer *user_model.User, issue *issues_model.Issue, oldContent string) { for _, notifier := range notifiers { notifier.NotifyIssueChangeContent(doer, issue, oldContent) } } // NotifyIssueChangeAssignee notifies change content to notifiers -func NotifyIssueChangeAssignee(doer *user_model.User, issue *models.Issue, assignee *user_model.User, removed bool, comment *models.Comment) { +func NotifyIssueChangeAssignee(doer *user_model.User, issue *issues_model.Issue, assignee *user_model.User, removed bool, comment *issues_model.Comment) { for _, notifier := range notifiers { notifier.NotifyIssueChangeAssignee(doer, issue, assignee, removed, comment) } } // NotifyPullReviewRequest notifies Request Review change -func NotifyPullReviewRequest(doer *user_model.User, issue *models.Issue, reviewer *user_model.User, isRequest bool, comment *models.Comment) { +func NotifyPullReviewRequest(doer *user_model.User, issue *issues_model.Issue, reviewer *user_model.User, isRequest bool, comment *issues_model.Comment) { for _, notifier := range notifiers { notifier.NotifyPullReviewRequest(doer, issue, reviewer, isRequest, comment) } } // NotifyIssueClearLabels notifies clear labels to notifiers -func NotifyIssueClearLabels(doer *user_model.User, issue *models.Issue) { +func NotifyIssueClearLabels(doer *user_model.User, issue *issues_model.Issue) { for _, notifier := range notifiers { notifier.NotifyIssueClearLabels(doer, issue) } } // NotifyIssueChangeTitle notifies change title to notifiers -func NotifyIssueChangeTitle(doer *user_model.User, issue *models.Issue, oldTitle string) { +func NotifyIssueChangeTitle(doer *user_model.User, issue *issues_model.Issue, oldTitle string) { for _, notifier := range notifiers { notifier.NotifyIssueChangeTitle(doer, issue, oldTitle) } } // NotifyIssueChangeRef notifies change reference to notifiers -func NotifyIssueChangeRef(doer *user_model.User, issue *models.Issue, oldRef string) { +func NotifyIssueChangeRef(doer *user_model.User, issue *issues_model.Issue, oldRef string) { for _, notifier := range notifiers { notifier.NotifyIssueChangeRef(doer, issue, oldRef) } } // NotifyIssueChangeLabels notifies change labels to notifiers -func NotifyIssueChangeLabels(doer *user_model.User, issue *models.Issue, - addedLabels, removedLabels []*models.Label, +func NotifyIssueChangeLabels(doer *user_model.User, issue *issues_model.Issue, + addedLabels, removedLabels []*issues_model.Label, ) { for _, notifier := range notifiers { notifier.NotifyIssueChangeLabels(doer, issue, addedLabels, removedLabels) diff --git a/modules/notification/ui/ui.go b/modules/notification/ui/ui.go index 037167f640..74866a3363 100644 --- a/modules/notification/ui/ui.go +++ b/modules/notification/ui/ui.go @@ -7,6 +7,7 @@ package ui import ( "code.gitea.io/gitea/models" "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/graceful" @@ -53,7 +54,7 @@ func (ns *notificationService) Run() { } func (ns *notificationService) NotifyCreateIssueComment(doer *user_model.User, repo *repo_model.Repository, - issue *models.Issue, comment *models.Comment, mentions []*user_model.User, + issue *issues_model.Issue, comment *issues_model.Comment, mentions []*user_model.User, ) { opts := issueNotificationOpts{ IssueID: issue.ID, @@ -76,7 +77,7 @@ func (ns *notificationService) NotifyCreateIssueComment(doer *user_model.User, r } } -func (ns *notificationService) NotifyNewIssue(issue *models.Issue, mentions []*user_model.User) { +func (ns *notificationService) NotifyNewIssue(issue *issues_model.Issue, mentions []*user_model.User) { _ = ns.issueQueue.Push(issueNotificationOpts{ IssueID: issue.ID, NotificationAuthorID: issue.Poster.ID, @@ -90,19 +91,19 @@ func (ns *notificationService) NotifyNewIssue(issue *models.Issue, mentions []*u } } -func (ns *notificationService) NotifyIssueChangeStatus(doer *user_model.User, issue *models.Issue, actionComment *models.Comment, isClosed bool) { +func (ns *notificationService) NotifyIssueChangeStatus(doer *user_model.User, issue *issues_model.Issue, actionComment *issues_model.Comment, isClosed bool) { _ = ns.issueQueue.Push(issueNotificationOpts{ IssueID: issue.ID, NotificationAuthorID: doer.ID, }) } -func (ns *notificationService) NotifyIssueChangeTitle(doer *user_model.User, issue *models.Issue, oldTitle string) { +func (ns *notificationService) NotifyIssueChangeTitle(doer *user_model.User, issue *issues_model.Issue, oldTitle string) { if err := issue.LoadPullRequest(); err != nil { log.Error("issue.LoadPullRequest: %v", err) return } - if issue.IsPull && models.HasWorkInProgressPrefix(oldTitle) && !issue.PullRequest.IsWorkInProgress() { + if issue.IsPull && issues_model.HasWorkInProgressPrefix(oldTitle) && !issue.PullRequest.IsWorkInProgress() { _ = ns.issueQueue.Push(issueNotificationOpts{ IssueID: issue.ID, NotificationAuthorID: doer.ID, @@ -110,14 +111,14 @@ func (ns *notificationService) NotifyIssueChangeTitle(doer *user_model.User, iss } } -func (ns *notificationService) NotifyMergePullRequest(pr *models.PullRequest, doer *user_model.User) { +func (ns *notificationService) NotifyMergePullRequest(pr *issues_model.PullRequest, doer *user_model.User) { _ = ns.issueQueue.Push(issueNotificationOpts{ IssueID: pr.Issue.ID, NotificationAuthorID: doer.ID, }) } -func (ns *notificationService) NotifyNewPullRequest(pr *models.PullRequest, mentions []*user_model.User) { +func (ns *notificationService) NotifyNewPullRequest(pr *issues_model.PullRequest, mentions []*user_model.User) { if err := pr.LoadIssue(); err != nil { log.Error("Unable to load issue: %d for pr: %d: Error: %v", pr.IssueID, pr.ID, err) return @@ -131,7 +132,7 @@ func (ns *notificationService) NotifyNewPullRequest(pr *models.PullRequest, ment for _, id := range repoWatchers { toNotify[id] = struct{}{} } - issueParticipants, err := models.GetParticipantsIDsByIssueID(pr.IssueID) + issueParticipants, err := issues_model.GetParticipantsIDsByIssueID(pr.IssueID) if err != nil { log.Error("GetParticipantsIDsByIssueID: %v", err) return @@ -152,7 +153,7 @@ func (ns *notificationService) NotifyNewPullRequest(pr *models.PullRequest, ment } } -func (ns *notificationService) NotifyPullRequestReview(pr *models.PullRequest, r *models.Review, c *models.Comment, mentions []*user_model.User) { +func (ns *notificationService) NotifyPullRequestReview(pr *issues_model.PullRequest, r *issues_model.Review, c *issues_model.Comment, mentions []*user_model.User) { opts := issueNotificationOpts{ IssueID: pr.Issue.ID, NotificationAuthorID: r.Reviewer.ID, @@ -174,7 +175,7 @@ func (ns *notificationService) NotifyPullRequestReview(pr *models.PullRequest, r } } -func (ns *notificationService) NotifyPullRequestCodeComment(pr *models.PullRequest, c *models.Comment, mentions []*user_model.User) { +func (ns *notificationService) NotifyPullRequestCodeComment(pr *issues_model.PullRequest, c *issues_model.Comment, mentions []*user_model.User) { for _, mention := range mentions { _ = ns.issueQueue.Push(issueNotificationOpts{ IssueID: pr.Issue.ID, @@ -185,7 +186,7 @@ func (ns *notificationService) NotifyPullRequestCodeComment(pr *models.PullReque } } -func (ns *notificationService) NotifyPullRequestPushCommits(doer *user_model.User, pr *models.PullRequest, comment *models.Comment) { +func (ns *notificationService) NotifyPullRequestPushCommits(doer *user_model.User, pr *issues_model.PullRequest, comment *issues_model.Comment) { opts := issueNotificationOpts{ IssueID: pr.IssueID, NotificationAuthorID: doer.ID, @@ -194,7 +195,7 @@ func (ns *notificationService) NotifyPullRequestPushCommits(doer *user_model.Use _ = ns.issueQueue.Push(opts) } -func (ns *notificationService) NotifyPullRevieweDismiss(doer *user_model.User, review *models.Review, comment *models.Comment) { +func (ns *notificationService) NotifyPullRevieweDismiss(doer *user_model.User, review *issues_model.Review, comment *issues_model.Comment) { opts := issueNotificationOpts{ IssueID: review.IssueID, NotificationAuthorID: doer.ID, @@ -203,7 +204,7 @@ func (ns *notificationService) NotifyPullRevieweDismiss(doer *user_model.User, r _ = ns.issueQueue.Push(opts) } -func (ns *notificationService) NotifyIssueChangeAssignee(doer *user_model.User, issue *models.Issue, assignee *user_model.User, removed bool, comment *models.Comment) { +func (ns *notificationService) NotifyIssueChangeAssignee(doer *user_model.User, issue *issues_model.Issue, assignee *user_model.User, removed bool, comment *issues_model.Comment) { if !removed && doer.ID != assignee.ID { opts := issueNotificationOpts{ IssueID: issue.ID, @@ -219,7 +220,7 @@ func (ns *notificationService) NotifyIssueChangeAssignee(doer *user_model.User, } } -func (ns *notificationService) NotifyPullReviewRequest(doer *user_model.User, issue *models.Issue, reviewer *user_model.User, isRequest bool, comment *models.Comment) { +func (ns *notificationService) NotifyPullReviewRequest(doer *user_model.User, issue *issues_model.Issue, reviewer *user_model.User, isRequest bool, comment *issues_model.Comment) { if isRequest { opts := issueNotificationOpts{ IssueID: issue.ID, diff --git a/modules/notification/webhook/webhook.go b/modules/notification/webhook/webhook.go index 38077f2180..be71d18fda 100644 --- a/modules/notification/webhook/webhook.go +++ b/modules/notification/webhook/webhook.go @@ -9,6 +9,7 @@ import ( "code.gitea.io/gitea/models" "code.gitea.io/gitea/models/db" + issues_model "code.gitea.io/gitea/models/issues" packages_model "code.gitea.io/gitea/models/packages" "code.gitea.io/gitea/models/perm" access_model "code.gitea.io/gitea/models/perm/access" @@ -39,7 +40,7 @@ func NewNotifier() base.Notifier { return &webhookNotifier{} } -func (m *webhookNotifier) NotifyIssueClearLabels(doer *user_model.User, issue *models.Issue) { +func (m *webhookNotifier) NotifyIssueClearLabels(doer *user_model.User, issue *issues_model.Issue) { ctx, _, finished := process.GetManager().AddContext(graceful.GetManager().HammerContext(), fmt.Sprintf("webhook.NotifyIssueClearLabels User: %s[%d] Issue[%d] #%d in [%d]", doer.Name, doer.ID, issue.ID, issue.Index, issue.RepoID)) defer finished() @@ -147,7 +148,7 @@ func (m *webhookNotifier) NotifyMigrateRepository(doer, u *user_model.User, repo } } -func (m *webhookNotifier) NotifyIssueChangeAssignee(doer *user_model.User, issue *models.Issue, assignee *user_model.User, removed bool, comment *models.Comment) { +func (m *webhookNotifier) NotifyIssueChangeAssignee(doer *user_model.User, issue *issues_model.Issue, assignee *user_model.User, removed bool, comment *issues_model.Comment) { ctx, _, finished := process.GetManager().AddContext(graceful.GetManager().HammerContext(), fmt.Sprintf("webhook.NotifyIssueChangeAssignee User: %s[%d] Issue[%d] #%d in [%d] Assignee %s[%d] removed: %t", doer.Name, doer.ID, issue.ID, issue.Index, issue.RepoID, assignee.Name, assignee.ID, removed)) defer finished() @@ -196,7 +197,7 @@ func (m *webhookNotifier) NotifyIssueChangeAssignee(doer *user_model.User, issue } } -func (m *webhookNotifier) NotifyIssueChangeTitle(doer *user_model.User, issue *models.Issue, oldTitle string) { +func (m *webhookNotifier) NotifyIssueChangeTitle(doer *user_model.User, issue *issues_model.Issue, oldTitle string) { ctx, _, finished := process.GetManager().AddContext(graceful.GetManager().HammerContext(), fmt.Sprintf("webhook.NotifyIssueChangeTitle User: %s[%d] Issue[%d] #%d in [%d]", doer.Name, doer.ID, issue.ID, issue.Index, issue.RepoID)) defer finished() @@ -240,7 +241,7 @@ func (m *webhookNotifier) NotifyIssueChangeTitle(doer *user_model.User, issue *m } } -func (m *webhookNotifier) NotifyIssueChangeStatus(doer *user_model.User, issue *models.Issue, actionComment *models.Comment, isClosed bool) { +func (m *webhookNotifier) NotifyIssueChangeStatus(doer *user_model.User, issue *issues_model.Issue, actionComment *issues_model.Comment, isClosed bool) { ctx, _, finished := process.GetManager().AddContext(graceful.GetManager().HammerContext(), fmt.Sprintf("webhook.NotifyIssueChangeStatus User: %s[%d] Issue[%d] #%d in [%d]", doer.Name, doer.ID, issue.ID, issue.Index, issue.RepoID)) defer finished() @@ -283,7 +284,7 @@ func (m *webhookNotifier) NotifyIssueChangeStatus(doer *user_model.User, issue * } } -func (m *webhookNotifier) NotifyNewIssue(issue *models.Issue, mentions []*user_model.User) { +func (m *webhookNotifier) NotifyNewIssue(issue *issues_model.Issue, mentions []*user_model.User) { if err := issue.LoadRepo(db.DefaultContext); err != nil { log.Error("issue.LoadRepo: %v", err) return @@ -305,7 +306,7 @@ func (m *webhookNotifier) NotifyNewIssue(issue *models.Issue, mentions []*user_m } } -func (m *webhookNotifier) NotifyNewPullRequest(pull *models.PullRequest, mentions []*user_model.User) { +func (m *webhookNotifier) NotifyNewPullRequest(pull *issues_model.PullRequest, mentions []*user_model.User) { ctx, _, finished := process.GetManager().AddContext(graceful.GetManager().HammerContext(), fmt.Sprintf("webhook.NotifyNewPullRequest Pull[%d] #%d in [%d]", pull.ID, pull.Index, pull.BaseRepoID)) defer finished() @@ -334,7 +335,7 @@ func (m *webhookNotifier) NotifyNewPullRequest(pull *models.PullRequest, mention } } -func (m *webhookNotifier) NotifyIssueChangeContent(doer *user_model.User, issue *models.Issue, oldContent string) { +func (m *webhookNotifier) NotifyIssueChangeContent(doer *user_model.User, issue *issues_model.Issue, oldContent string) { ctx, _, finished := process.GetManager().AddContext(graceful.GetManager().HammerContext(), fmt.Sprintf("webhook.NotifyIssueChangeContent User: %s[%d] Issue[%d] #%d in [%d]", doer.Name, doer.ID, issue.ID, issue.Index, issue.RepoID)) defer finished() @@ -373,7 +374,7 @@ func (m *webhookNotifier) NotifyIssueChangeContent(doer *user_model.User, issue } } -func (m *webhookNotifier) NotifyUpdateComment(doer *user_model.User, c *models.Comment, oldContent string) { +func (m *webhookNotifier) NotifyUpdateComment(doer *user_model.User, c *issues_model.Comment, oldContent string) { var err error if err = c.LoadPoster(); err != nil { @@ -385,7 +386,7 @@ func (m *webhookNotifier) NotifyUpdateComment(doer *user_model.User, c *models.C return } - if err = c.Issue.LoadAttributes(); err != nil { + if err = c.Issue.LoadAttributes(db.DefaultContext); err != nil { log.Error("LoadAttributes: %v", err) return } @@ -427,7 +428,7 @@ func (m *webhookNotifier) NotifyUpdateComment(doer *user_model.User, c *models.C } func (m *webhookNotifier) NotifyCreateIssueComment(doer *user_model.User, repo *repo_model.Repository, - issue *models.Issue, comment *models.Comment, mentions []*user_model.User, + issue *issues_model.Issue, comment *issues_model.Comment, mentions []*user_model.User, ) { mode, _ := access_model.AccessLevel(doer, repo) @@ -457,7 +458,7 @@ func (m *webhookNotifier) NotifyCreateIssueComment(doer *user_model.User, repo * } } -func (m *webhookNotifier) NotifyDeleteComment(doer *user_model.User, comment *models.Comment) { +func (m *webhookNotifier) NotifyDeleteComment(doer *user_model.User, comment *issues_model.Comment) { var err error if err = comment.LoadPoster(); err != nil { @@ -469,7 +470,7 @@ func (m *webhookNotifier) NotifyDeleteComment(doer *user_model.User, comment *mo return } - if err = comment.Issue.LoadAttributes(); err != nil { + if err = comment.Issue.LoadAttributes(db.DefaultContext); err != nil { log.Error("LoadAttributes: %v", err) return } @@ -501,8 +502,8 @@ func (m *webhookNotifier) NotifyDeleteComment(doer *user_model.User, comment *mo } } -func (m *webhookNotifier) NotifyIssueChangeLabels(doer *user_model.User, issue *models.Issue, - addedLabels, removedLabels []*models.Label, +func (m *webhookNotifier) NotifyIssueChangeLabels(doer *user_model.User, issue *issues_model.Issue, + addedLabels, removedLabels []*issues_model.Label, ) { ctx, _, finished := process.GetManager().AddContext(graceful.GetManager().HammerContext(), fmt.Sprintf("webhook.NotifyIssueChangeLabels User: %s[%d] Issue[%d] #%d in [%d]", doer.Name, doer.ID, issue.ID, issue.Index, issue.RepoID)) defer finished() @@ -550,7 +551,7 @@ func (m *webhookNotifier) NotifyIssueChangeLabels(doer *user_model.User, issue * } } -func (m *webhookNotifier) NotifyIssueChangeMilestone(doer *user_model.User, issue *models.Issue, oldMilestoneID int64) { +func (m *webhookNotifier) NotifyIssueChangeMilestone(doer *user_model.User, issue *issues_model.Issue, oldMilestoneID int64) { ctx, _, finished := process.GetManager().AddContext(graceful.GetManager().HammerContext(), fmt.Sprintf("webhook.NotifyIssueChangeMilestone User: %s[%d] Issue[%d] #%d in [%d]", doer.Name, doer.ID, issue.ID, issue.Index, issue.RepoID)) defer finished() @@ -562,7 +563,7 @@ func (m *webhookNotifier) NotifyIssueChangeMilestone(doer *user_model.User, issu hookAction = api.HookIssueDemilestoned } - if err = issue.LoadAttributes(); err != nil { + if err = issue.LoadAttributes(db.DefaultContext); err != nil { log.Error("issue.LoadAttributes failed: %v", err) return } @@ -621,7 +622,7 @@ func (m *webhookNotifier) NotifyPushCommits(pusher *user_model.User, repo *repo_ } } -func (*webhookNotifier) NotifyMergePullRequest(pr *models.PullRequest, doer *user_model.User) { +func (*webhookNotifier) NotifyMergePullRequest(pr *issues_model.PullRequest, doer *user_model.User) { ctx, _, finished := process.GetManager().AddContext(graceful.GetManager().HammerContext(), fmt.Sprintf("webhook.NotifyMergePullRequest Pull[%d] #%d in [%d]", pr.ID, pr.Index, pr.BaseRepoID)) defer finished() @@ -662,7 +663,7 @@ func (*webhookNotifier) NotifyMergePullRequest(pr *models.PullRequest, doer *use } } -func (m *webhookNotifier) NotifyPullRequestChangeTargetBranch(doer *user_model.User, pr *models.PullRequest, oldBranch string) { +func (m *webhookNotifier) NotifyPullRequestChangeTargetBranch(doer *user_model.User, pr *issues_model.PullRequest, oldBranch string) { ctx, _, finished := process.GetManager().AddContext(graceful.GetManager().HammerContext(), fmt.Sprintf("webhook.NotifyPullRequestChangeTargetBranch Pull[%d] #%d in [%d]", pr.ID, pr.Index, pr.BaseRepoID)) defer finished() @@ -696,18 +697,18 @@ func (m *webhookNotifier) NotifyPullRequestChangeTargetBranch(doer *user_model.U } } -func (m *webhookNotifier) NotifyPullRequestReview(pr *models.PullRequest, review *models.Review, comment *models.Comment, mentions []*user_model.User) { +func (m *webhookNotifier) NotifyPullRequestReview(pr *issues_model.PullRequest, review *issues_model.Review, comment *issues_model.Comment, mentions []*user_model.User) { ctx, _, finished := process.GetManager().AddContext(graceful.GetManager().HammerContext(), fmt.Sprintf("webhook.NotifyPullRequestReview Pull[%d] #%d in [%d]", pr.ID, pr.Index, pr.BaseRepoID)) defer finished() var reviewHookType webhook.HookEventType switch review.Type { - case models.ReviewTypeApprove: + case issues_model.ReviewTypeApprove: reviewHookType = webhook.HookEventPullRequestReviewApproved - case models.ReviewTypeComment: + case issues_model.ReviewTypeComment: reviewHookType = webhook.HookEventPullRequestComment - case models.ReviewTypeReject: + case issues_model.ReviewTypeReject: reviewHookType = webhook.HookEventPullRequestReviewRejected default: // unsupported review webhook type here @@ -756,7 +757,7 @@ func (m *webhookNotifier) NotifyCreateRef(pusher *user_model.User, repo *repo_mo } } -func (m *webhookNotifier) NotifyPullRequestSynchronized(doer *user_model.User, pr *models.PullRequest) { +func (m *webhookNotifier) NotifyPullRequestSynchronized(doer *user_model.User, pr *issues_model.PullRequest) { ctx, _, finished := process.GetManager().AddContext(graceful.GetManager().HammerContext(), fmt.Sprintf("webhook.NotifyPullRequestSynchronized Pull[%d] #%d in [%d]", pr.ID, pr.Index, pr.BaseRepoID)) defer finished() @@ -764,7 +765,7 @@ func (m *webhookNotifier) NotifyPullRequestSynchronized(doer *user_model.User, p log.Error("pr.LoadIssue: %v", err) return } - if err := pr.Issue.LoadAttributes(); err != nil { + if err := pr.Issue.LoadAttributes(db.DefaultContext); err != nil { log.Error("LoadAttributes: %v", err) return } diff --git a/modules/repository/init.go b/modules/repository/init.go index 285fe81dbe..f5cef3301d 100644 --- a/modules/repository/init.go +++ b/modules/repository/init.go @@ -16,6 +16,7 @@ import ( "time" "code.gitea.io/gitea/models" + 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" @@ -113,7 +114,7 @@ func GetLabelTemplateFile(name string) ([][3]string, error) { if len(color) == 6 { color = "#" + color } - if !models.LabelColorPattern.MatchString(color) { + if !issues_model.LabelColorPattern.MatchString(color) { return nil, ErrIssueLabelTemplateLoad{name, fmt.Errorf("bad HTML color code in line: %s", line)} } @@ -453,9 +454,9 @@ func InitializeLabels(ctx context.Context, id int64, labelTemplate string, isOrg return err } - labels := make([]*models.Label, len(list)) + labels := make([]*issues_model.Label, len(list)) for i := 0; i < len(list); i++ { - labels[i] = &models.Label{ + labels[i] = &issues_model.Label{ Name: list[i][0], Description: list[i][2], Color: list[i][1], @@ -467,7 +468,7 @@ func InitializeLabels(ctx context.Context, id int64, labelTemplate string, isOrg } } for _, label := range labels { - if err = models.NewLabel(ctx, label); err != nil { + if err = issues_model.NewLabel(ctx, label); err != nil { return err } } diff --git a/modules/templates/helper.go b/modules/templates/helper.go index c0be5c1fa5..b77928fc9c 100644 --- a/modules/templates/helper.go +++ b/modules/templates/helper.go @@ -26,6 +26,7 @@ import ( "code.gitea.io/gitea/models" "code.gitea.io/gitea/models/avatars" + issues_model "code.gitea.io/gitea/models/issues" "code.gitea.io/gitea/models/organization" repo_model "code.gitea.io/gitea/models/repo" user_model "code.gitea.io/gitea/models/user" @@ -374,7 +375,7 @@ func NewFuncMap() []template.FuncMap { // the table is NOT sorted with this header return "" }, - "RenderLabels": func(labels []*models.Label) template.HTML { + "RenderLabels": func(labels []*issues_model.Label) template.HTML { html := `` for _, label := range labels { // Protect against nil value in labels - shouldn't happen but would cause a panic if so diff --git a/routers/api/v1/misc/nodeinfo.go b/routers/api/v1/misc/nodeinfo.go index c786544e14..bd629b87ca 100644 --- a/routers/api/v1/misc/nodeinfo.go +++ b/routers/api/v1/misc/nodeinfo.go @@ -8,7 +8,7 @@ import ( "net/http" "time" - "code.gitea.io/gitea/models" + issues_model "code.gitea.io/gitea/models/issues" user_model "code.gitea.io/gitea/models/user" "code.gitea.io/gitea/modules/context" "code.gitea.io/gitea/modules/setting" @@ -42,8 +42,8 @@ func NodeInfo(ctx *context.APIContext) { usersActiveMonth := int(user_model.CountUsers(&user_model.CountUserFilter{LastLoginSince: &timeOneMonthAgo})) usersActiveHalfyear := int(user_model.CountUsers(&user_model.CountUserFilter{LastLoginSince: &timeHaveYearAgo})) - allIssues, _ := models.CountIssues(&models.IssuesOptions{}) - allComments, _ := models.CountComments(&models.FindCommentsOptions{}) + allIssues, _ := issues_model.CountIssues(&issues_model.IssuesOptions{}) + allComments, _ := issues_model.CountComments(&issues_model.FindCommentsOptions{}) nodeInfoUsage = structs.NodeInfoUsage{ Users: structs.NodeInfoUsageUsers{ diff --git a/routers/api/v1/notify/threads.go b/routers/api/v1/notify/threads.go index 4effd6b3e0..7d8d34504f 100644 --- a/routers/api/v1/notify/threads.go +++ b/routers/api/v1/notify/threads.go @@ -10,6 +10,7 @@ import ( "code.gitea.io/gitea/models" "code.gitea.io/gitea/models/db" + issues_model "code.gitea.io/gitea/models/issues" "code.gitea.io/gitea/modules/context" "code.gitea.io/gitea/modules/convert" ) @@ -41,7 +42,7 @@ func GetThread(ctx *context.APIContext) { if n == nil { return } - if err := n.LoadAttributes(); err != nil && !models.IsErrCommentNotExist(err) { + if err := n.LoadAttributes(); err != nil && !issues_model.IsErrCommentNotExist(err) { ctx.InternalServerError(err) return } @@ -93,7 +94,7 @@ func ReadThread(ctx *context.APIContext) { ctx.InternalServerError(err) return } - if err = notif.LoadAttributes(); err != nil && !models.IsErrCommentNotExist(err) { + if err = notif.LoadAttributes(); err != nil && !issues_model.IsErrCommentNotExist(err) { ctx.InternalServerError(err) return } diff --git a/routers/api/v1/org/label.go b/routers/api/v1/org/label.go index 9844ea21d2..a67bd56dfc 100644 --- a/routers/api/v1/org/label.go +++ b/routers/api/v1/org/label.go @@ -10,7 +10,7 @@ import ( "strconv" "strings" - "code.gitea.io/gitea/models" + issues_model "code.gitea.io/gitea/models/issues" "code.gitea.io/gitea/modules/context" "code.gitea.io/gitea/modules/convert" api "code.gitea.io/gitea/modules/structs" @@ -43,13 +43,13 @@ func ListLabels(ctx *context.APIContext) { // "200": // "$ref": "#/responses/LabelList" - labels, err := models.GetLabelsByOrgID(ctx, ctx.Org.Organization.ID, ctx.FormString("sort"), utils.GetListOptions(ctx)) + labels, err := issues_model.GetLabelsByOrgID(ctx, ctx.Org.Organization.ID, ctx.FormString("sort"), utils.GetListOptions(ctx)) if err != nil { ctx.Error(http.StatusInternalServerError, "GetLabelsByOrgID", err) return } - count, err := models.CountLabelsByOrgID(ctx.Org.Organization.ID) + count, err := issues_model.CountLabelsByOrgID(ctx.Org.Organization.ID) if err != nil { ctx.InternalServerError(err) return @@ -88,18 +88,18 @@ func CreateLabel(ctx *context.APIContext) { if len(form.Color) == 6 { form.Color = "#" + form.Color } - if !models.LabelColorPattern.MatchString(form.Color) { + if !issues_model.LabelColorPattern.MatchString(form.Color) { ctx.Error(http.StatusUnprocessableEntity, "ColorPattern", fmt.Errorf("bad color code: %s", form.Color)) return } - label := &models.Label{ + label := &issues_model.Label{ Name: form.Name, Color: form.Color, OrgID: ctx.Org.Organization.ID, Description: form.Description, } - if err := models.NewLabel(ctx, label); err != nil { + if err := issues_model.NewLabel(ctx, label); err != nil { ctx.Error(http.StatusInternalServerError, "NewLabel", err) return } @@ -131,17 +131,17 @@ func GetLabel(ctx *context.APIContext) { // "$ref": "#/responses/Label" var ( - label *models.Label + label *issues_model.Label err error ) strID := ctx.Params(":id") if intID, err2 := strconv.ParseInt(strID, 10, 64); err2 != nil { - label, err = models.GetLabelInOrgByName(ctx, ctx.Org.Organization.ID, strID) + label, err = issues_model.GetLabelInOrgByName(ctx, ctx.Org.Organization.ID, strID) } else { - label, err = models.GetLabelInOrgByID(ctx, ctx.Org.Organization.ID, intID) + label, err = issues_model.GetLabelInOrgByID(ctx, ctx.Org.Organization.ID, intID) } if err != nil { - if models.IsErrOrgLabelNotExist(err) { + if issues_model.IsErrOrgLabelNotExist(err) { ctx.NotFound() } else { ctx.Error(http.StatusInternalServerError, "GetLabelByOrgID", err) @@ -183,9 +183,9 @@ func EditLabel(ctx *context.APIContext) { // "422": // "$ref": "#/responses/validationError" form := web.GetForm(ctx).(*api.EditLabelOption) - label, err := models.GetLabelInOrgByID(ctx, ctx.Org.Organization.ID, ctx.ParamsInt64(":id")) + label, err := issues_model.GetLabelInOrgByID(ctx, ctx.Org.Organization.ID, ctx.ParamsInt64(":id")) if err != nil { - if models.IsErrOrgLabelNotExist(err) { + if issues_model.IsErrOrgLabelNotExist(err) { ctx.NotFound() } else { ctx.Error(http.StatusInternalServerError, "GetLabelByRepoID", err) @@ -201,7 +201,7 @@ func EditLabel(ctx *context.APIContext) { if len(label.Color) == 6 { label.Color = "#" + label.Color } - if !models.LabelColorPattern.MatchString(label.Color) { + if !issues_model.LabelColorPattern.MatchString(label.Color) { ctx.Error(http.StatusUnprocessableEntity, "ColorPattern", fmt.Errorf("bad color code: %s", label.Color)) return } @@ -209,7 +209,7 @@ func EditLabel(ctx *context.APIContext) { if form.Description != nil { label.Description = *form.Description } - if err := models.UpdateLabel(label); err != nil { + if err := issues_model.UpdateLabel(label); err != nil { ctx.Error(http.StatusInternalServerError, "UpdateLabel", err) return } @@ -238,7 +238,7 @@ func DeleteLabel(ctx *context.APIContext) { // "204": // "$ref": "#/responses/empty" - if err := models.DeleteLabel(ctx.Org.Organization.ID, ctx.ParamsInt64(":id")); err != nil { + if err := issues_model.DeleteLabel(ctx.Org.Organization.ID, ctx.ParamsInt64(":id")); err != nil { ctx.Error(http.StatusInternalServerError, "DeleteLabel", err) return } diff --git a/routers/api/v1/repo/file.go b/routers/api/v1/repo/file.go index ffa3ddc784..2190094bac 100644 --- a/routers/api/v1/repo/file.go +++ b/routers/api/v1/repo/file.go @@ -557,7 +557,7 @@ func handleCreateOrUpdateFileError(ctx *context.APIContext, err error) { // Called from both CreateFile or UpdateFile to handle both func createOrUpdateFile(ctx *context.APIContext, opts *files_service.UpdateRepoFileOptions) (*api.FileResponse, error) { if !canWriteFiles(ctx, opts.OldBranch) { - return nil, models.ErrUserDoesNotHaveAccessToRepo{ + return nil, repo_model.ErrUserDoesNotHaveAccessToRepo{ UserID: ctx.Doer.ID, RepoName: ctx.Repo.Repository.LowerName, } @@ -614,7 +614,7 @@ func DeleteFile(ctx *context.APIContext) { apiOpts := web.GetForm(ctx).(*api.DeleteFileOptions) if !canWriteFiles(ctx, apiOpts.BranchName) { - ctx.Error(http.StatusForbidden, "DeleteFile", models.ErrUserDoesNotHaveAccessToRepo{ + ctx.Error(http.StatusForbidden, "DeleteFile", repo_model.ErrUserDoesNotHaveAccessToRepo{ UserID: ctx.Doer.ID, RepoName: ctx.Repo.Repository.LowerName, }) @@ -712,7 +712,7 @@ func GetContents(ctx *context.APIContext) { // "$ref": "#/responses/notFound" if !canReadFiles(ctx.Repo) { - ctx.Error(http.StatusInternalServerError, "GetContentsOrList", models.ErrUserDoesNotHaveAccessToRepo{ + ctx.Error(http.StatusInternalServerError, "GetContentsOrList", repo_model.ErrUserDoesNotHaveAccessToRepo{ UserID: ctx.Doer.ID, RepoName: ctx.Repo.Repository.LowerName, }) diff --git a/routers/api/v1/repo/issue.go b/routers/api/v1/repo/issue.go index c394ad1756..ddad18ef62 100644 --- a/routers/api/v1/repo/issue.go +++ b/routers/api/v1/repo/issue.go @@ -12,7 +12,6 @@ import ( "strings" "time" - "code.gitea.io/gitea/models" "code.gitea.io/gitea/models/db" issues_model "code.gitea.io/gitea/models/issues" "code.gitea.io/gitea/models/organization" @@ -184,7 +183,7 @@ func SearchIssues(ctx *context.APIContext) { return } - var issues []*models.Issue + var issues []*issues_model.Issue var filteredCount int64 keyword := ctx.FormTrim("q") @@ -233,7 +232,7 @@ func SearchIssues(ctx *context.APIContext) { // Only fetch the issues if we either don't have a keyword or the search returned issues // This would otherwise return all issues if no issues were found by the search. if len(keyword) == 0 || len(issueIDs) > 0 || len(includedLabelNames) > 0 || len(includedMilestones) > 0 { - issuesOpt := &models.IssuesOptions{ + issuesOpt := &issues_model.IssuesOptions{ ListOptions: db.ListOptions{ Page: ctx.FormInt("page"), PageSize: limit, @@ -269,7 +268,7 @@ func SearchIssues(ctx *context.APIContext) { issuesOpt.ReviewRequestedID = ctxUserID } - if issues, err = models.Issues(issuesOpt); err != nil { + if issues, err = issues_model.Issues(issuesOpt); err != nil { ctx.Error(http.StatusInternalServerError, "Issues", err) return } @@ -277,7 +276,7 @@ func SearchIssues(ctx *context.APIContext) { issuesOpt.ListOptions = db.ListOptions{ Page: -1, } - if filteredCount, err = models.CountIssues(issuesOpt); err != nil { + if filteredCount, err = issues_model.CountIssues(issuesOpt); err != nil { ctx.Error(http.StatusInternalServerError, "CountIssues", err) return } @@ -379,7 +378,7 @@ func ListIssues(ctx *context.APIContext) { isClosed = util.OptionalBoolFalse } - var issues []*models.Issue + var issues []*issues_model.Issue var filteredCount int64 keyword := ctx.FormTrim("q") @@ -397,7 +396,7 @@ func ListIssues(ctx *context.APIContext) { } if splitted := strings.Split(ctx.FormString("labels"), ","); len(splitted) > 0 { - labelIDs, err = models.GetLabelIDsInRepoByNames(ctx.Repo.Repository.ID, splitted) + labelIDs, err = issues_model.GetLabelIDsInRepoByNames(ctx.Repo.Repository.ID, splitted) if err != nil { ctx.Error(http.StatusInternalServerError, "GetLabelIDsInRepoByNames", err) return @@ -463,7 +462,7 @@ func ListIssues(ctx *context.APIContext) { // Only fetch the issues if we either don't have a keyword or the search returned issues // This would otherwise return all issues if no issues were found by the search. if len(keyword) == 0 || len(issueIDs) > 0 || len(labelIDs) > 0 { - issuesOpt := &models.IssuesOptions{ + issuesOpt := &issues_model.IssuesOptions{ ListOptions: listOptions, RepoID: ctx.Repo.Repository.ID, IsClosed: isClosed, @@ -478,7 +477,7 @@ func ListIssues(ctx *context.APIContext) { MentionedID: mentionedByID, } - if issues, err = models.Issues(issuesOpt); err != nil { + if issues, err = issues_model.Issues(issuesOpt); err != nil { ctx.Error(http.StatusInternalServerError, "Issues", err) return } @@ -486,7 +485,7 @@ func ListIssues(ctx *context.APIContext) { issuesOpt.ListOptions = db.ListOptions{ Page: -1, } - if filteredCount, err = models.CountIssues(issuesOpt); err != nil { + if filteredCount, err = issues_model.CountIssues(issuesOpt); err != nil { ctx.Error(http.StatusInternalServerError, "CountIssues", err) return } @@ -547,9 +546,9 @@ func GetIssue(ctx *context.APIContext) { // "404": // "$ref": "#/responses/notFound" - issue, err := models.GetIssueWithAttrsByIndex(ctx.Repo.Repository.ID, ctx.ParamsInt64(":index")) + issue, err := issues_model.GetIssueWithAttrsByIndex(ctx.Repo.Repository.ID, ctx.ParamsInt64(":index")) if err != nil { - if models.IsErrIssueNotExist(err) { + if issues_model.IsErrIssueNotExist(err) { ctx.NotFound() } else { ctx.Error(http.StatusInternalServerError, "GetIssueByIndex", err) @@ -598,7 +597,7 @@ func CreateIssue(ctx *context.APIContext) { deadlineUnix = timeutil.TimeStamp(form.Deadline.Unix()) } - issue := &models.Issue{ + issue := &issues_model.Issue{ RepoID: ctx.Repo.Repository.ID, Repo: ctx.Repo.Repository, Title: form.Title, @@ -613,7 +612,7 @@ func CreateIssue(ctx *context.APIContext) { var err error if ctx.Repo.CanWrite(unit.TypeIssues) { issue.MilestoneID = form.Milestone - assigneeIDs, err = models.MakeIDsFromAPIAssigneesToAdd(form.Assignee, form.Assignees) + assigneeIDs, err = issues_model.MakeIDsFromAPIAssigneesToAdd(form.Assignee, form.Assignees) if err != nil { if user_model.IsErrUserNotExist(err) { ctx.Error(http.StatusUnprocessableEntity, "", fmt.Sprintf("Assignee does not exist: [name: %s]", err)) @@ -637,7 +636,7 @@ func CreateIssue(ctx *context.APIContext) { return } if !valid { - ctx.Error(http.StatusUnprocessableEntity, "canBeAssigned", models.ErrUserDoesNotHaveAccessToRepo{UserID: aID, RepoName: ctx.Repo.Repository.Name}) + ctx.Error(http.StatusUnprocessableEntity, "canBeAssigned", repo_model.ErrUserDoesNotHaveAccessToRepo{UserID: aID, RepoName: ctx.Repo.Repository.Name}) return } } @@ -647,7 +646,7 @@ func CreateIssue(ctx *context.APIContext) { } if err := issue_service.NewIssue(ctx.Repo.Repository, issue, form.Labels, nil, assigneeIDs); err != nil { - if models.IsErrUserDoesNotHaveAccessToRepo(err) { + if repo_model.IsErrUserDoesNotHaveAccessToRepo(err) { ctx.Error(http.StatusBadRequest, "UserDoesNotHaveAccessToRepo", err) return } @@ -657,7 +656,7 @@ func CreateIssue(ctx *context.APIContext) { if form.Closed { if err := issue_service.ChangeStatus(issue, ctx.Doer, true); err != nil { - if models.IsErrDependenciesLeft(err) { + if issues_model.IsErrDependenciesLeft(err) { ctx.Error(http.StatusPreconditionFailed, "DependenciesLeft", "cannot close this issue because it still has open dependencies") return } @@ -667,7 +666,7 @@ func CreateIssue(ctx *context.APIContext) { } // Refetch from database to assign some automatic values - issue, err = models.GetIssueByID(issue.ID) + issue, err = issues_model.GetIssueByID(ctx, issue.ID) if err != nil { ctx.Error(http.StatusInternalServerError, "GetIssueByID", err) return @@ -716,9 +715,9 @@ func EditIssue(ctx *context.APIContext) { // "$ref": "#/responses/error" form := web.GetForm(ctx).(*api.EditIssueOption) - issue, err := models.GetIssueByIndex(ctx.Repo.Repository.ID, ctx.ParamsInt64(":index")) + issue, err := issues_model.GetIssueByIndex(ctx.Repo.Repository.ID, ctx.ParamsInt64(":index")) if err != nil { - if models.IsErrIssueNotExist(err) { + if issues_model.IsErrIssueNotExist(err) { ctx.NotFound() } else { ctx.Error(http.StatusInternalServerError, "GetIssueByIndex", err) @@ -728,7 +727,7 @@ func EditIssue(ctx *context.APIContext) { issue.Repo = ctx.Repo.Repository canWrite := ctx.Repo.CanWriteIssuesOrPulls(issue.IsPull) - err = issue.LoadAttributes() + err = issue.LoadAttributes(ctx) if err != nil { ctx.Error(http.StatusInternalServerError, "LoadAttributes", err) return @@ -764,7 +763,7 @@ func EditIssue(ctx *context.APIContext) { deadlineUnix = timeutil.TimeStamp(deadline.Unix()) } - if err := models.UpdateIssueDeadline(issue, deadlineUnix, ctx.Doer); err != nil { + if err := issues_model.UpdateIssueDeadline(issue, deadlineUnix, ctx.Doer); err != nil { ctx.Error(http.StatusInternalServerError, "UpdateIssueDeadline", err) return } @@ -813,9 +812,9 @@ func EditIssue(ctx *context.APIContext) { } issue.IsClosed = api.StateClosed == api.StateType(*form.State) } - statusChangeComment, titleChanged, err := models.UpdateIssueByAPI(issue, ctx.Doer) + statusChangeComment, titleChanged, err := issues_model.UpdateIssueByAPI(issue, ctx.Doer) if err != nil { - if models.IsErrDependenciesLeft(err) { + if issues_model.IsErrDependenciesLeft(err) { ctx.Error(http.StatusPreconditionFailed, "DependenciesLeft", "cannot close this issue because it still has open dependencies") return } @@ -832,7 +831,7 @@ func EditIssue(ctx *context.APIContext) { } // Refetch from database to assign some automatic values - issue, err = models.GetIssueByID(issue.ID) + issue, err = issues_model.GetIssueByID(ctx, issue.ID) if err != nil { ctx.InternalServerError(err) return @@ -872,9 +871,9 @@ func DeleteIssue(ctx *context.APIContext) { // "$ref": "#/responses/forbidden" // "404": // "$ref": "#/responses/notFound" - issue, err := models.GetIssueByIndex(ctx.Repo.Repository.ID, ctx.ParamsInt64(":index")) + issue, err := issues_model.GetIssueByIndex(ctx.Repo.Repository.ID, ctx.ParamsInt64(":index")) if err != nil { - if models.IsErrIssueNotExist(err) { + if issues_model.IsErrIssueNotExist(err) { ctx.NotFound(err) } else { ctx.Error(http.StatusInternalServerError, "GetIssueByID", err) @@ -928,9 +927,9 @@ func UpdateIssueDeadline(ctx *context.APIContext) { // "404": // "$ref": "#/responses/notFound" form := web.GetForm(ctx).(*api.EditDeadlineOption) - issue, err := models.GetIssueByIndex(ctx.Repo.Repository.ID, ctx.ParamsInt64(":index")) + issue, err := issues_model.GetIssueByIndex(ctx.Repo.Repository.ID, ctx.ParamsInt64(":index")) if err != nil { - if models.IsErrIssueNotExist(err) { + if issues_model.IsErrIssueNotExist(err) { ctx.NotFound() } else { ctx.Error(http.StatusInternalServerError, "GetIssueByIndex", err) @@ -951,7 +950,7 @@ func UpdateIssueDeadline(ctx *context.APIContext) { deadlineUnix = timeutil.TimeStamp(deadline.Unix()) } - if err := models.UpdateIssueDeadline(issue, deadlineUnix, ctx.Doer); err != nil { + if err := issues_model.UpdateIssueDeadline(issue, deadlineUnix, ctx.Doer); err != nil { ctx.Error(http.StatusInternalServerError, "UpdateIssueDeadline", err) return } diff --git a/routers/api/v1/repo/issue_comment.go b/routers/api/v1/repo/issue_comment.go index 22533c3810..89038e4f16 100644 --- a/routers/api/v1/repo/issue_comment.go +++ b/routers/api/v1/repo/issue_comment.go @@ -10,7 +10,7 @@ import ( "errors" "net/http" - "code.gitea.io/gitea/models" + issues_model "code.gitea.io/gitea/models/issues" access_model "code.gitea.io/gitea/models/perm/access" repo_model "code.gitea.io/gitea/models/repo" user_model "code.gitea.io/gitea/models/user" @@ -65,33 +65,33 @@ func ListIssueComments(ctx *context.APIContext) { ctx.Error(http.StatusUnprocessableEntity, "GetQueryBeforeSince", err) return } - issue, err := models.GetIssueByIndex(ctx.Repo.Repository.ID, ctx.ParamsInt64(":index")) + issue, err := issues_model.GetIssueByIndex(ctx.Repo.Repository.ID, ctx.ParamsInt64(":index")) if err != nil { ctx.Error(http.StatusInternalServerError, "GetRawIssueByIndex", err) return } issue.Repo = ctx.Repo.Repository - opts := &models.FindCommentsOptions{ + opts := &issues_model.FindCommentsOptions{ IssueID: issue.ID, Since: since, Before: before, - Type: models.CommentTypeComment, + Type: issues_model.CommentTypeComment, } - comments, err := models.FindComments(ctx, opts) + comments, err := issues_model.FindComments(ctx, opts) if err != nil { ctx.Error(http.StatusInternalServerError, "FindComments", err) return } - totalCount, err := models.CountComments(opts) + totalCount, err := issues_model.CountComments(opts) if err != nil { ctx.InternalServerError(err) return } - if err := models.CommentList(comments).LoadPosters(); err != nil { + if err := issues_model.CommentList(comments).LoadPosters(); err != nil { ctx.Error(http.StatusInternalServerError, "LoadPosters", err) return } @@ -157,35 +157,35 @@ func ListIssueCommentsAndTimeline(ctx *context.APIContext) { ctx.Error(http.StatusUnprocessableEntity, "GetQueryBeforeSince", err) return } - issue, err := models.GetIssueByIndex(ctx.Repo.Repository.ID, ctx.ParamsInt64(":index")) + issue, err := issues_model.GetIssueByIndex(ctx.Repo.Repository.ID, ctx.ParamsInt64(":index")) if err != nil { ctx.Error(http.StatusInternalServerError, "GetRawIssueByIndex", err) return } issue.Repo = ctx.Repo.Repository - opts := &models.FindCommentsOptions{ + opts := &issues_model.FindCommentsOptions{ ListOptions: utils.GetListOptions(ctx), IssueID: issue.ID, Since: since, Before: before, - Type: models.CommentTypeUnknown, + Type: issues_model.CommentTypeUnknown, } - comments, err := models.FindComments(ctx, opts) + comments, err := issues_model.FindComments(ctx, opts) if err != nil { ctx.Error(http.StatusInternalServerError, "FindComments", err) return } - if err := models.CommentList(comments).LoadPosters(); err != nil { + if err := issues_model.CommentList(comments).LoadPosters(); err != nil { ctx.Error(http.StatusInternalServerError, "LoadPosters", err) return } var apiComments []*api.TimelineComment for _, comment := range comments { - if comment.Type != models.CommentTypeCode && isXRefCommentAccessible(ctx, ctx.Doer, comment, issue.RepoID) { + if comment.Type != issues_model.CommentTypeCode && isXRefCommentAccessible(ctx, ctx.Doer, comment, issue.RepoID) { comment.Issue = issue apiComments = append(apiComments, convert.ToTimelineComment(comment, ctx.Doer)) } @@ -195,9 +195,9 @@ func ListIssueCommentsAndTimeline(ctx *context.APIContext) { ctx.JSON(http.StatusOK, &apiComments) } -func isXRefCommentAccessible(ctx stdCtx.Context, user *user_model.User, c *models.Comment, issueRepoID int64) bool { +func isXRefCommentAccessible(ctx stdCtx.Context, user *user_model.User, c *issues_model.Comment, issueRepoID int64) bool { // Remove comments that the user has no permissions to see - if models.CommentTypeIsRef(c.Type) && c.RefRepoID != issueRepoID && c.RefRepoID != 0 { + if issues_model.CommentTypeIsRef(c.Type) && c.RefRepoID != issueRepoID && c.RefRepoID != 0 { var err error // Set RefRepo for description in template c.RefRepo, err = repo_model.GetRepositoryByIDCtx(ctx, c.RefRepoID) @@ -261,41 +261,41 @@ func ListRepoIssueComments(ctx *context.APIContext) { return } - opts := &models.FindCommentsOptions{ + opts := &issues_model.FindCommentsOptions{ ListOptions: utils.GetListOptions(ctx), RepoID: ctx.Repo.Repository.ID, - Type: models.CommentTypeComment, + Type: issues_model.CommentTypeComment, Since: since, Before: before, } - comments, err := models.FindComments(ctx, opts) + comments, err := issues_model.FindComments(ctx, opts) if err != nil { ctx.Error(http.StatusInternalServerError, "FindComments", err) return } - totalCount, err := models.CountComments(opts) + totalCount, err := issues_model.CountComments(opts) if err != nil { ctx.InternalServerError(err) return } - if err = models.CommentList(comments).LoadPosters(); err != nil { + if err = issues_model.CommentList(comments).LoadPosters(); err != nil { ctx.Error(http.StatusInternalServerError, "LoadPosters", err) return } apiComments := make([]*api.Comment, len(comments)) - if err := models.CommentList(comments).LoadIssues(); err != nil { + if err := issues_model.CommentList(comments).LoadIssues(); err != nil { ctx.Error(http.StatusInternalServerError, "LoadIssues", err) return } - if err := models.CommentList(comments).LoadPosters(); err != nil { + if err := issues_model.CommentList(comments).LoadPosters(); err != nil { ctx.Error(http.StatusInternalServerError, "LoadPosters", err) return } - if _, err := models.CommentList(comments).Issues().LoadRepositories(); err != nil { + if _, err := issues_model.CommentList(comments).Issues().LoadRepositories(); err != nil { ctx.Error(http.StatusInternalServerError, "LoadRepositories", err) return } @@ -343,7 +343,7 @@ func CreateIssueComment(ctx *context.APIContext) { // "403": // "$ref": "#/responses/forbidden" form := web.GetForm(ctx).(*api.CreateIssueCommentOption) - issue, err := models.GetIssueByIndex(ctx.Repo.Repository.ID, ctx.ParamsInt64(":index")) + issue, err := issues_model.GetIssueByIndex(ctx.Repo.Repository.ID, ctx.ParamsInt64(":index")) if err != nil { ctx.Error(http.StatusInternalServerError, "GetIssueByIndex", err) return @@ -399,9 +399,9 @@ func GetIssueComment(ctx *context.APIContext) { // "404": // "$ref": "#/responses/notFound" - comment, err := models.GetCommentByID(ctx, ctx.ParamsInt64(":id")) + comment, err := issues_model.GetCommentByID(ctx, ctx.ParamsInt64(":id")) if err != nil { - if models.IsErrCommentNotExist(err) { + if issues_model.IsErrCommentNotExist(err) { ctx.NotFound(err) } else { ctx.Error(http.StatusInternalServerError, "GetCommentByID", err) @@ -418,7 +418,7 @@ func GetIssueComment(ctx *context.APIContext) { return } - if comment.Type != models.CommentTypeComment { + if comment.Type != issues_model.CommentTypeComment { ctx.Status(http.StatusNoContent) return } @@ -526,9 +526,9 @@ func EditIssueCommentDeprecated(ctx *context.APIContext) { } func editIssueComment(ctx *context.APIContext, form api.EditIssueCommentOption) { - comment, err := models.GetCommentByID(ctx, ctx.ParamsInt64(":id")) + comment, err := issues_model.GetCommentByID(ctx, ctx.ParamsInt64(":id")) if err != nil { - if models.IsErrCommentNotExist(err) { + if issues_model.IsErrCommentNotExist(err) { ctx.NotFound(err) } else { ctx.Error(http.StatusInternalServerError, "GetCommentByID", err) @@ -541,7 +541,7 @@ func editIssueComment(ctx *context.APIContext, form api.EditIssueCommentOption) return } - if comment.Type != models.CommentTypeComment && comment.Type != models.CommentTypeReview && comment.Type != models.CommentTypeCode { + if comment.Type != issues_model.CommentTypeComment && comment.Type != issues_model.CommentTypeReview && comment.Type != issues_model.CommentTypeCode { ctx.Status(http.StatusNoContent) return } @@ -629,9 +629,9 @@ func DeleteIssueCommentDeprecated(ctx *context.APIContext) { } func deleteIssueComment(ctx *context.APIContext) { - comment, err := models.GetCommentByID(ctx, ctx.ParamsInt64(":id")) + comment, err := issues_model.GetCommentByID(ctx, ctx.ParamsInt64(":id")) if err != nil { - if models.IsErrCommentNotExist(err) { + if issues_model.IsErrCommentNotExist(err) { ctx.NotFound(err) } else { ctx.Error(http.StatusInternalServerError, "GetCommentByID", err) @@ -642,7 +642,7 @@ func deleteIssueComment(ctx *context.APIContext) { if !ctx.IsSigned || (ctx.Doer.ID != comment.PosterID && !ctx.Repo.IsAdmin()) { ctx.Status(http.StatusForbidden) return - } else if comment.Type != models.CommentTypeComment { + } else if comment.Type != issues_model.CommentTypeComment { ctx.Status(http.StatusNoContent) return } diff --git a/routers/api/v1/repo/issue_label.go b/routers/api/v1/repo/issue_label.go index 0193eb4230..50c09e02fa 100644 --- a/routers/api/v1/repo/issue_label.go +++ b/routers/api/v1/repo/issue_label.go @@ -8,7 +8,7 @@ package repo import ( "net/http" - "code.gitea.io/gitea/models" + issues_model "code.gitea.io/gitea/models/issues" "code.gitea.io/gitea/modules/context" "code.gitea.io/gitea/modules/convert" api "code.gitea.io/gitea/modules/structs" @@ -46,9 +46,9 @@ func ListIssueLabels(ctx *context.APIContext) { // "404": // "$ref": "#/responses/notFound" - issue, err := models.GetIssueByIndex(ctx.Repo.Repository.ID, ctx.ParamsInt64(":index")) + issue, err := issues_model.GetIssueByIndex(ctx.Repo.Repository.ID, ctx.ParamsInt64(":index")) if err != nil { - if models.IsErrIssueNotExist(err) { + if issues_model.IsErrIssueNotExist(err) { ctx.NotFound() } else { ctx.Error(http.StatusInternalServerError, "GetIssueByIndex", err) @@ -56,7 +56,7 @@ func ListIssueLabels(ctx *context.APIContext) { return } - if err := issue.LoadAttributes(); err != nil { + if err := issue.LoadAttributes(ctx); err != nil { ctx.Error(http.StatusInternalServerError, "LoadAttributes", err) return } @@ -111,7 +111,7 @@ func AddIssueLabels(ctx *context.APIContext) { return } - labels, err = models.GetLabelsByIssueID(ctx, issue.ID) + labels, err = issues_model.GetLabelsByIssueID(ctx, issue.ID) if err != nil { ctx.Error(http.StatusInternalServerError, "GetLabelsByIssueID", err) return @@ -158,9 +158,9 @@ func DeleteIssueLabel(ctx *context.APIContext) { // "422": // "$ref": "#/responses/validationError" - issue, err := models.GetIssueByIndex(ctx.Repo.Repository.ID, ctx.ParamsInt64(":index")) + issue, err := issues_model.GetIssueByIndex(ctx.Repo.Repository.ID, ctx.ParamsInt64(":index")) if err != nil { - if models.IsErrIssueNotExist(err) { + if issues_model.IsErrIssueNotExist(err) { ctx.NotFound() } else { ctx.Error(http.StatusInternalServerError, "GetIssueByIndex", err) @@ -173,9 +173,9 @@ func DeleteIssueLabel(ctx *context.APIContext) { return } - label, err := models.GetLabelByID(ctx, ctx.ParamsInt64(":id")) + label, err := issues_model.GetLabelByID(ctx, ctx.ParamsInt64(":id")) if err != nil { - if models.IsErrLabelNotExist(err) { + if issues_model.IsErrLabelNotExist(err) { ctx.Error(http.StatusUnprocessableEntity, "", err) } else { ctx.Error(http.StatusInternalServerError, "GetLabelByID", err) @@ -237,7 +237,7 @@ func ReplaceIssueLabels(ctx *context.APIContext) { return } - labels, err = models.GetLabelsByIssueID(ctx, issue.ID) + labels, err = issues_model.GetLabelsByIssueID(ctx, issue.ID) if err != nil { ctx.Error(http.StatusInternalServerError, "GetLabelsByIssueID", err) return @@ -276,9 +276,9 @@ func ClearIssueLabels(ctx *context.APIContext) { // "403": // "$ref": "#/responses/forbidden" - issue, err := models.GetIssueByIndex(ctx.Repo.Repository.ID, ctx.ParamsInt64(":index")) + issue, err := issues_model.GetIssueByIndex(ctx.Repo.Repository.ID, ctx.ParamsInt64(":index")) if err != nil { - if models.IsErrIssueNotExist(err) { + if issues_model.IsErrIssueNotExist(err) { ctx.NotFound() } else { ctx.Error(http.StatusInternalServerError, "GetIssueByIndex", err) @@ -299,10 +299,10 @@ func ClearIssueLabels(ctx *context.APIContext) { ctx.Status(http.StatusNoContent) } -func prepareForReplaceOrAdd(ctx *context.APIContext, form api.IssueLabelsOption) (issue *models.Issue, labels []*models.Label, err error) { - issue, err = models.GetIssueByIndex(ctx.Repo.Repository.ID, ctx.ParamsInt64(":index")) +func prepareForReplaceOrAdd(ctx *context.APIContext, form api.IssueLabelsOption) (issue *issues_model.Issue, labels []*issues_model.Label, err error) { + issue, err = issues_model.GetIssueByIndex(ctx.Repo.Repository.ID, ctx.ParamsInt64(":index")) if err != nil { - if models.IsErrIssueNotExist(err) { + if issues_model.IsErrIssueNotExist(err) { ctx.NotFound() } else { ctx.Error(http.StatusInternalServerError, "GetIssueByIndex", err) @@ -310,7 +310,7 @@ func prepareForReplaceOrAdd(ctx *context.APIContext, form api.IssueLabelsOption) return } - labels, err = models.GetLabelsByIDs(form.Labels) + labels, err = issues_model.GetLabelsByIDs(form.Labels) if err != nil { ctx.Error(http.StatusInternalServerError, "GetLabelsByIDs", err) return diff --git a/routers/api/v1/repo/issue_reaction.go b/routers/api/v1/repo/issue_reaction.go index 45be7a92dd..f4c40d2bcd 100644 --- a/routers/api/v1/repo/issue_reaction.go +++ b/routers/api/v1/repo/issue_reaction.go @@ -8,7 +8,6 @@ import ( "errors" "net/http" - "code.gitea.io/gitea/models" issues_model "code.gitea.io/gitea/models/issues" "code.gitea.io/gitea/modules/context" "code.gitea.io/gitea/modules/convert" @@ -49,9 +48,9 @@ func GetIssueCommentReactions(ctx *context.APIContext) { // "403": // "$ref": "#/responses/forbidden" - comment, err := models.GetCommentByID(ctx, ctx.ParamsInt64(":id")) + comment, err := issues_model.GetCommentByID(ctx, ctx.ParamsInt64(":id")) if err != nil { - if models.IsErrCommentNotExist(err) { + if issues_model.IsErrCommentNotExist(err) { ctx.NotFound(err) } else { ctx.Error(http.StatusInternalServerError, "GetCommentByID", err) @@ -176,9 +175,9 @@ func DeleteIssueCommentReaction(ctx *context.APIContext) { } func changeIssueCommentReaction(ctx *context.APIContext, form api.EditReactionOption, isCreateType bool) { - comment, err := models.GetCommentByID(ctx, ctx.ParamsInt64(":id")) + comment, err := issues_model.GetCommentByID(ctx, ctx.ParamsInt64(":id")) if err != nil { - if models.IsErrCommentNotExist(err) { + if issues_model.IsErrCommentNotExist(err) { ctx.NotFound(err) } else { ctx.Error(http.StatusInternalServerError, "GetCommentByID", err) @@ -271,9 +270,9 @@ func GetIssueReactions(ctx *context.APIContext) { // "403": // "$ref": "#/responses/forbidden" - issue, err := models.GetIssueWithAttrsByIndex(ctx.Repo.Repository.ID, ctx.ParamsInt64(":index")) + issue, err := issues_model.GetIssueWithAttrsByIndex(ctx.Repo.Repository.ID, ctx.ParamsInt64(":index")) if err != nil { - if models.IsErrIssueNotExist(err) { + if issues_model.IsErrIssueNotExist(err) { ctx.NotFound() } else { ctx.Error(http.StatusInternalServerError, "GetIssueByIndex", err) @@ -391,9 +390,9 @@ func DeleteIssueReaction(ctx *context.APIContext) { } func changeIssueReaction(ctx *context.APIContext, form api.EditReactionOption, isCreateType bool) { - issue, err := models.GetIssueWithAttrsByIndex(ctx.Repo.Repository.ID, ctx.ParamsInt64(":index")) + issue, err := issues_model.GetIssueWithAttrsByIndex(ctx.Repo.Repository.ID, ctx.ParamsInt64(":index")) if err != nil { - if models.IsErrIssueNotExist(err) { + if issues_model.IsErrIssueNotExist(err) { ctx.NotFound() } else { ctx.Error(http.StatusInternalServerError, "GetIssueByIndex", err) diff --git a/routers/api/v1/repo/issue_stopwatch.go b/routers/api/v1/repo/issue_stopwatch.go index 382f294346..941b22e454 100644 --- a/routers/api/v1/repo/issue_stopwatch.go +++ b/routers/api/v1/repo/issue_stopwatch.go @@ -8,7 +8,7 @@ import ( "errors" "net/http" - "code.gitea.io/gitea/models" + issues_model "code.gitea.io/gitea/models/issues" "code.gitea.io/gitea/modules/context" "code.gitea.io/gitea/modules/convert" "code.gitea.io/gitea/routers/api/v1/utils" @@ -55,7 +55,7 @@ func StartIssueStopwatch(ctx *context.APIContext) { return } - if err := models.CreateIssueStopwatch(ctx, ctx.Doer, issue); err != nil { + if err := issues_model.CreateIssueStopwatch(ctx, ctx.Doer, issue); err != nil { ctx.Error(http.StatusInternalServerError, "CreateOrStopIssueStopwatch", err) return } @@ -104,7 +104,7 @@ func StopIssueStopwatch(ctx *context.APIContext) { return } - if err := models.FinishIssueStopwatch(ctx, ctx.Doer, issue); err != nil { + if err := issues_model.FinishIssueStopwatch(ctx, ctx.Doer, issue); err != nil { ctx.Error(http.StatusInternalServerError, "CreateOrStopIssueStopwatch", err) return } @@ -153,7 +153,7 @@ func DeleteIssueStopwatch(ctx *context.APIContext) { return } - if err := models.CancelStopwatch(ctx.Doer, issue); err != nil { + if err := issues_model.CancelStopwatch(ctx.Doer, issue); err != nil { ctx.Error(http.StatusInternalServerError, "CancelStopwatch", err) return } @@ -161,10 +161,10 @@ func DeleteIssueStopwatch(ctx *context.APIContext) { ctx.Status(http.StatusNoContent) } -func prepareIssueStopwatch(ctx *context.APIContext, shouldExist bool) (*models.Issue, error) { - issue, err := models.GetIssueByIndex(ctx.Repo.Repository.ID, ctx.ParamsInt64(":index")) +func prepareIssueStopwatch(ctx *context.APIContext, shouldExist bool) (*issues_model.Issue, error) { + issue, err := issues_model.GetIssueByIndex(ctx.Repo.Repository.ID, ctx.ParamsInt64(":index")) if err != nil { - if models.IsErrIssueNotExist(err) { + if issues_model.IsErrIssueNotExist(err) { ctx.NotFound() } else { ctx.Error(http.StatusInternalServerError, "GetIssueByIndex", err) @@ -183,7 +183,7 @@ func prepareIssueStopwatch(ctx *context.APIContext, shouldExist bool) (*models.I return nil, errors.New("Cannot use time tracker") } - if models.StopwatchExists(ctx.Doer.ID, issue.ID) != shouldExist { + if issues_model.StopwatchExists(ctx.Doer.ID, issue.ID) != shouldExist { if shouldExist { ctx.Error(http.StatusConflict, "StopwatchExists", "cannot stop/cancel a non existent stopwatch") err = errors.New("cannot stop/cancel a non existent stopwatch") @@ -219,13 +219,13 @@ func GetStopwatches(ctx *context.APIContext) { // "200": // "$ref": "#/responses/StopWatchList" - sws, err := models.GetUserStopwatches(ctx.Doer.ID, utils.GetListOptions(ctx)) + sws, err := issues_model.GetUserStopwatches(ctx.Doer.ID, utils.GetListOptions(ctx)) if err != nil { ctx.Error(http.StatusInternalServerError, "GetUserStopwatches", err) return } - count, err := models.CountUserStopwatches(ctx.Doer.ID) + count, err := issues_model.CountUserStopwatches(ctx.Doer.ID) if err != nil { ctx.InternalServerError(err) return diff --git a/routers/api/v1/repo/issue_subscription.go b/routers/api/v1/repo/issue_subscription.go index a608ba2278..5e03e42b4c 100644 --- a/routers/api/v1/repo/issue_subscription.go +++ b/routers/api/v1/repo/issue_subscription.go @@ -8,7 +8,7 @@ import ( "fmt" "net/http" - "code.gitea.io/gitea/models" + issues_model "code.gitea.io/gitea/models/issues" user_model "code.gitea.io/gitea/models/user" "code.gitea.io/gitea/modules/context" "code.gitea.io/gitea/modules/convert" @@ -105,9 +105,9 @@ func DelIssueSubscription(ctx *context.APIContext) { } func setIssueSubscription(ctx *context.APIContext, watch bool) { - issue, err := models.GetIssueByIndex(ctx.Repo.Repository.ID, ctx.ParamsInt64(":index")) + issue, err := issues_model.GetIssueByIndex(ctx.Repo.Repository.ID, ctx.ParamsInt64(":index")) if err != nil { - if models.IsErrIssueNotExist(err) { + if issues_model.IsErrIssueNotExist(err) { ctx.NotFound() } else { ctx.Error(http.StatusInternalServerError, "GetIssueByIndex", err) @@ -133,7 +133,7 @@ func setIssueSubscription(ctx *context.APIContext, watch bool) { return } - current, err := models.CheckIssueWatch(user, issue) + current, err := issues_model.CheckIssueWatch(user, issue) if err != nil { ctx.Error(http.StatusInternalServerError, "CheckIssueWatch", err) return @@ -146,7 +146,7 @@ func setIssueSubscription(ctx *context.APIContext, watch bool) { } // Update watch state - if err := models.CreateOrUpdateIssueWatch(user.ID, issue.ID, watch); err != nil { + if err := issues_model.CreateOrUpdateIssueWatch(user.ID, issue.ID, watch); err != nil { ctx.Error(http.StatusInternalServerError, "CreateOrUpdateIssueWatch", err) return } @@ -186,9 +186,9 @@ func CheckIssueSubscription(ctx *context.APIContext) { // "404": // "$ref": "#/responses/notFound" - issue, err := models.GetIssueByIndex(ctx.Repo.Repository.ID, ctx.ParamsInt64(":index")) + issue, err := issues_model.GetIssueByIndex(ctx.Repo.Repository.ID, ctx.ParamsInt64(":index")) if err != nil { - if models.IsErrIssueNotExist(err) { + if issues_model.IsErrIssueNotExist(err) { ctx.NotFound() } else { ctx.Error(http.StatusInternalServerError, "GetIssueByIndex", err) @@ -197,7 +197,7 @@ func CheckIssueSubscription(ctx *context.APIContext) { return } - watching, err := models.CheckIssueWatch(ctx.Doer, issue) + watching, err := issues_model.CheckIssueWatch(ctx.Doer, issue) if err != nil { ctx.InternalServerError(err) return @@ -252,9 +252,9 @@ func GetIssueSubscribers(ctx *context.APIContext) { // "404": // "$ref": "#/responses/notFound" - issue, err := models.GetIssueByIndex(ctx.Repo.Repository.ID, ctx.ParamsInt64(":index")) + issue, err := issues_model.GetIssueByIndex(ctx.Repo.Repository.ID, ctx.ParamsInt64(":index")) if err != nil { - if models.IsErrIssueNotExist(err) { + if issues_model.IsErrIssueNotExist(err) { ctx.NotFound() } else { ctx.Error(http.StatusInternalServerError, "GetIssueByIndex", err) @@ -263,7 +263,7 @@ func GetIssueSubscribers(ctx *context.APIContext) { return } - iwl, err := models.GetIssueWatchers(ctx, issue.ID, utils.GetListOptions(ctx)) + iwl, err := issues_model.GetIssueWatchers(ctx, issue.ID, utils.GetListOptions(ctx)) if err != nil { ctx.Error(http.StatusInternalServerError, "GetIssueWatchers", err) return @@ -284,7 +284,7 @@ func GetIssueSubscribers(ctx *context.APIContext) { apiUsers = append(apiUsers, convert.ToUser(v, ctx.Doer)) } - count, err := models.CountIssueWatchers(ctx, issue.ID) + count, err := issues_model.CountIssueWatchers(ctx, issue.ID) if err != nil { ctx.Error(http.StatusInternalServerError, "CountIssueWatchers", err) return diff --git a/routers/api/v1/repo/issue_tracked_time.go b/routers/api/v1/repo/issue_tracked_time.go index 19e1a82590..b9a6c5af64 100644 --- a/routers/api/v1/repo/issue_tracked_time.go +++ b/routers/api/v1/repo/issue_tracked_time.go @@ -9,8 +9,8 @@ import ( "net/http" "time" - "code.gitea.io/gitea/models" "code.gitea.io/gitea/models/db" + issues_model "code.gitea.io/gitea/models/issues" "code.gitea.io/gitea/models/unit" user_model "code.gitea.io/gitea/models/user" "code.gitea.io/gitea/modules/context" @@ -76,9 +76,9 @@ func ListTrackedTimes(ctx *context.APIContext) { ctx.NotFound("Timetracker is disabled") return } - issue, err := models.GetIssueByIndex(ctx.Repo.Repository.ID, ctx.ParamsInt64(":index")) + issue, err := issues_model.GetIssueByIndex(ctx.Repo.Repository.ID, ctx.ParamsInt64(":index")) if err != nil { - if models.IsErrIssueNotExist(err) { + if issues_model.IsErrIssueNotExist(err) { ctx.NotFound(err) } else { ctx.Error(http.StatusInternalServerError, "GetIssueByIndex", err) @@ -86,7 +86,7 @@ func ListTrackedTimes(ctx *context.APIContext) { return } - opts := &models.FindTrackedTimesOptions{ + opts := &issues_model.FindTrackedTimesOptions{ ListOptions: utils.GetListOptions(ctx), RepositoryID: ctx.Repo.Repository.ID, IssueID: issue.ID, @@ -122,13 +122,13 @@ func ListTrackedTimes(ctx *context.APIContext) { } } - count, err := models.CountTrackedTimes(opts) + count, err := issues_model.CountTrackedTimes(opts) if err != nil { ctx.InternalServerError(err) return } - trackedTimes, err := models.GetTrackedTimes(ctx, opts) + trackedTimes, err := issues_model.GetTrackedTimes(ctx, opts) if err != nil { ctx.Error(http.StatusInternalServerError, "GetTrackedTimes", err) return @@ -180,9 +180,9 @@ func AddTime(ctx *context.APIContext) { // "403": // "$ref": "#/responses/forbidden" form := web.GetForm(ctx).(*api.AddTimeOption) - issue, err := models.GetIssueByIndex(ctx.Repo.Repository.ID, ctx.ParamsInt64(":index")) + issue, err := issues_model.GetIssueByIndex(ctx.Repo.Repository.ID, ctx.ParamsInt64(":index")) if err != nil { - if models.IsErrIssueNotExist(err) { + if issues_model.IsErrIssueNotExist(err) { ctx.NotFound(err) } else { ctx.Error(http.StatusInternalServerError, "GetIssueByIndex", err) @@ -215,7 +215,7 @@ func AddTime(ctx *context.APIContext) { created = form.Created } - trackedTime, err := models.AddTime(user, issue, form.Time, created) + trackedTime, err := issues_model.AddTime(user, issue, form.Time, created) if err != nil { ctx.Error(http.StatusInternalServerError, "AddTime", err) return @@ -261,9 +261,9 @@ func ResetIssueTime(ctx *context.APIContext) { // "403": // "$ref": "#/responses/forbidden" - issue, err := models.GetIssueByIndex(ctx.Repo.Repository.ID, ctx.ParamsInt64(":index")) + issue, err := issues_model.GetIssueByIndex(ctx.Repo.Repository.ID, ctx.ParamsInt64(":index")) if err != nil { - if models.IsErrIssueNotExist(err) { + if issues_model.IsErrIssueNotExist(err) { ctx.NotFound(err) } else { ctx.Error(http.StatusInternalServerError, "GetIssueByIndex", err) @@ -280,7 +280,7 @@ func ResetIssueTime(ctx *context.APIContext) { return } - err = models.DeleteIssueUserTimes(issue, ctx.Doer) + err = issues_model.DeleteIssueUserTimes(issue, ctx.Doer) if err != nil { if db.IsErrNotExist(err) { ctx.Error(http.StatusNotFound, "DeleteIssueUserTimes", err) @@ -332,9 +332,9 @@ func DeleteTime(ctx *context.APIContext) { // "403": // "$ref": "#/responses/forbidden" - issue, err := models.GetIssueByIndex(ctx.Repo.Repository.ID, ctx.ParamsInt64(":index")) + issue, err := issues_model.GetIssueByIndex(ctx.Repo.Repository.ID, ctx.ParamsInt64(":index")) if err != nil { - if models.IsErrIssueNotExist(err) { + if issues_model.IsErrIssueNotExist(err) { ctx.NotFound(err) } else { ctx.Error(http.StatusInternalServerError, "GetIssueByIndex", err) @@ -351,7 +351,7 @@ func DeleteTime(ctx *context.APIContext) { return } - time, err := models.GetTrackedTimeByID(ctx.ParamsInt64(":id")) + time, err := issues_model.GetTrackedTimeByID(ctx.ParamsInt64(":id")) if err != nil { if db.IsErrNotExist(err) { ctx.NotFound(err) @@ -371,7 +371,7 @@ func DeleteTime(ctx *context.APIContext) { return } - err = models.DeleteTime(time) + err = issues_model.DeleteTime(time) if err != nil { ctx.Error(http.StatusInternalServerError, "DeleteTime", err) return @@ -434,12 +434,12 @@ func ListTrackedTimesByUser(ctx *context.APIContext) { return } - opts := &models.FindTrackedTimesOptions{ + opts := &issues_model.FindTrackedTimesOptions{ UserID: user.ID, RepositoryID: ctx.Repo.Repository.ID, } - trackedTimes, err := models.GetTrackedTimes(ctx, opts) + trackedTimes, err := issues_model.GetTrackedTimes(ctx, opts) if err != nil { ctx.Error(http.StatusInternalServerError, "GetTrackedTimes", err) return @@ -504,7 +504,7 @@ func ListTrackedTimesByRepository(ctx *context.APIContext) { return } - opts := &models.FindTrackedTimesOptions{ + opts := &issues_model.FindTrackedTimesOptions{ ListOptions: utils.GetListOptions(ctx), RepositoryID: ctx.Repo.Repository.ID, } @@ -541,13 +541,13 @@ func ListTrackedTimesByRepository(ctx *context.APIContext) { } } - count, err := models.CountTrackedTimes(opts) + count, err := issues_model.CountTrackedTimes(opts) if err != nil { ctx.InternalServerError(err) return } - trackedTimes, err := models.GetTrackedTimes(ctx, opts) + trackedTimes, err := issues_model.GetTrackedTimes(ctx, opts) if err != nil { ctx.Error(http.StatusInternalServerError, "GetTrackedTimes", err) return @@ -592,7 +592,7 @@ func ListMyTrackedTimes(ctx *context.APIContext) { // "200": // "$ref": "#/responses/TrackedTimeList" - opts := &models.FindTrackedTimesOptions{ + opts := &issues_model.FindTrackedTimesOptions{ ListOptions: utils.GetListOptions(ctx), UserID: ctx.Doer.ID, } @@ -603,13 +603,13 @@ func ListMyTrackedTimes(ctx *context.APIContext) { return } - count, err := models.CountTrackedTimes(opts) + count, err := issues_model.CountTrackedTimes(opts) if err != nil { ctx.InternalServerError(err) return } - trackedTimes, err := models.GetTrackedTimes(ctx, opts) + trackedTimes, err := issues_model.GetTrackedTimes(ctx, opts) if err != nil { ctx.Error(http.StatusInternalServerError, "GetTrackedTimesByUser", err) return diff --git a/routers/api/v1/repo/label.go b/routers/api/v1/repo/label.go index 4332b8e627..8b1e298668 100644 --- a/routers/api/v1/repo/label.go +++ b/routers/api/v1/repo/label.go @@ -11,7 +11,7 @@ import ( "strconv" "strings" - "code.gitea.io/gitea/models" + issues_model "code.gitea.io/gitea/models/issues" "code.gitea.io/gitea/modules/context" "code.gitea.io/gitea/modules/convert" api "code.gitea.io/gitea/modules/structs" @@ -49,13 +49,13 @@ func ListLabels(ctx *context.APIContext) { // "200": // "$ref": "#/responses/LabelList" - labels, err := models.GetLabelsByRepoID(ctx, ctx.Repo.Repository.ID, ctx.FormString("sort"), utils.GetListOptions(ctx)) + labels, err := issues_model.GetLabelsByRepoID(ctx, ctx.Repo.Repository.ID, ctx.FormString("sort"), utils.GetListOptions(ctx)) if err != nil { ctx.Error(http.StatusInternalServerError, "GetLabelsByRepoID", err) return } - count, err := models.CountLabelsByRepoID(ctx.Repo.Repository.ID) + count, err := issues_model.CountLabelsByRepoID(ctx.Repo.Repository.ID) if err != nil { ctx.InternalServerError(err) return @@ -94,17 +94,17 @@ func GetLabel(ctx *context.APIContext) { // "$ref": "#/responses/Label" var ( - label *models.Label + label *issues_model.Label err error ) strID := ctx.Params(":id") if intID, err2 := strconv.ParseInt(strID, 10, 64); err2 != nil { - label, err = models.GetLabelInRepoByName(ctx, ctx.Repo.Repository.ID, strID) + label, err = issues_model.GetLabelInRepoByName(ctx, ctx.Repo.Repository.ID, strID) } else { - label, err = models.GetLabelInRepoByID(ctx, ctx.Repo.Repository.ID, intID) + label, err = issues_model.GetLabelInRepoByID(ctx, ctx.Repo.Repository.ID, intID) } if err != nil { - if models.IsErrRepoLabelNotExist(err) { + if issues_model.IsErrRepoLabelNotExist(err) { ctx.NotFound() } else { ctx.Error(http.StatusInternalServerError, "GetLabelByRepoID", err) @@ -150,18 +150,18 @@ func CreateLabel(ctx *context.APIContext) { if len(form.Color) == 6 { form.Color = "#" + form.Color } - if !models.LabelColorPattern.MatchString(form.Color) { + if !issues_model.LabelColorPattern.MatchString(form.Color) { ctx.Error(http.StatusUnprocessableEntity, "ColorPattern", fmt.Errorf("bad color code: %s", form.Color)) return } - label := &models.Label{ + label := &issues_model.Label{ Name: form.Name, Color: form.Color, RepoID: ctx.Repo.Repository.ID, Description: form.Description, } - if err := models.NewLabel(ctx, label); err != nil { + if err := issues_model.NewLabel(ctx, label); err != nil { ctx.Error(http.StatusInternalServerError, "NewLabel", err) return } @@ -206,9 +206,9 @@ func EditLabel(ctx *context.APIContext) { // "$ref": "#/responses/validationError" form := web.GetForm(ctx).(*api.EditLabelOption) - label, err := models.GetLabelInRepoByID(ctx, ctx.Repo.Repository.ID, ctx.ParamsInt64(":id")) + label, err := issues_model.GetLabelInRepoByID(ctx, ctx.Repo.Repository.ID, ctx.ParamsInt64(":id")) if err != nil { - if models.IsErrRepoLabelNotExist(err) { + if issues_model.IsErrRepoLabelNotExist(err) { ctx.NotFound() } else { ctx.Error(http.StatusInternalServerError, "GetLabelByRepoID", err) @@ -224,7 +224,7 @@ func EditLabel(ctx *context.APIContext) { if len(label.Color) == 6 { label.Color = "#" + label.Color } - if !models.LabelColorPattern.MatchString(label.Color) { + if !issues_model.LabelColorPattern.MatchString(label.Color) { ctx.Error(http.StatusUnprocessableEntity, "ColorPattern", fmt.Errorf("bad color code: %s", label.Color)) return } @@ -232,7 +232,7 @@ func EditLabel(ctx *context.APIContext) { if form.Description != nil { label.Description = *form.Description } - if err := models.UpdateLabel(label); err != nil { + if err := issues_model.UpdateLabel(label); err != nil { ctx.Error(http.StatusInternalServerError, "UpdateLabel", err) return } @@ -266,7 +266,7 @@ func DeleteLabel(ctx *context.APIContext) { // "204": // "$ref": "#/responses/empty" - if err := models.DeleteLabel(ctx.Repo.Repository.ID, ctx.ParamsInt64(":id")); err != nil { + if err := issues_model.DeleteLabel(ctx.Repo.Repository.ID, ctx.ParamsInt64(":id")); err != nil { ctx.Error(http.StatusInternalServerError, "DeleteLabel", err) return } diff --git a/routers/api/v1/repo/patch.go b/routers/api/v1/repo/patch.go index 6dbf979701..40afe2d92b 100644 --- a/routers/api/v1/repo/patch.go +++ b/routers/api/v1/repo/patch.go @@ -9,6 +9,7 @@ import ( "time" "code.gitea.io/gitea/models" + repo_model "code.gitea.io/gitea/models/repo" "code.gitea.io/gitea/modules/context" "code.gitea.io/gitea/modules/git" api "code.gitea.io/gitea/modules/structs" @@ -78,7 +79,7 @@ func ApplyDiffPatch(ctx *context.APIContext) { } if !canWriteFiles(ctx, apiOpts.BranchName) { - ctx.Error(http.StatusInternalServerError, "ApplyPatch", models.ErrUserDoesNotHaveAccessToRepo{ + ctx.Error(http.StatusInternalServerError, "ApplyPatch", repo_model.ErrUserDoesNotHaveAccessToRepo{ UserID: ctx.Doer.ID, RepoName: ctx.Repo.Repository.LowerName, }) diff --git a/routers/api/v1/repo/pull.go b/routers/api/v1/repo/pull.go index 393f2d1576..50d2c9484f 100644 --- a/routers/api/v1/repo/pull.go +++ b/routers/api/v1/repo/pull.go @@ -92,7 +92,7 @@ func ListPullRequests(ctx *context.APIContext) { listOptions := utils.GetListOptions(ctx) - prs, maxResults, err := models.PullRequests(ctx.Repo.Repository.ID, &models.PullRequestsOptions{ + prs, maxResults, err := issues_model.PullRequests(ctx.Repo.Repository.ID, &issues_model.PullRequestsOptions{ ListOptions: listOptions, State: ctx.FormTrim("state"), SortType: ctx.FormTrim("sort"), @@ -160,9 +160,9 @@ func GetPullRequest(ctx *context.APIContext) { // "404": // "$ref": "#/responses/notFound" - pr, err := models.GetPullRequestByIndex(ctx, ctx.Repo.Repository.ID, ctx.ParamsInt64(":index")) + pr, err := issues_model.GetPullRequestByIndex(ctx, ctx.Repo.Repository.ID, ctx.ParamsInt64(":index")) if err != nil { - if models.IsErrPullRequestNotExist(err) { + if issues_model.IsErrPullRequestNotExist(err) { ctx.NotFound() } else { ctx.Error(http.StatusInternalServerError, "GetPullRequestByIndex", err) @@ -220,9 +220,9 @@ func DownloadPullDiffOrPatch(ctx *context.APIContext) { // "$ref": "#/responses/string" // "404": // "$ref": "#/responses/notFound" - pr, err := models.GetPullRequestByIndex(ctx, ctx.Repo.Repository.ID, ctx.ParamsInt64(":index")) + pr, err := issues_model.GetPullRequestByIndex(ctx, ctx.Repo.Repository.ID, ctx.ParamsInt64(":index")) if err != nil { - if models.IsErrPullRequestNotExist(err) { + if issues_model.IsErrPullRequestNotExist(err) { ctx.NotFound() } else { ctx.InternalServerError(err) @@ -297,14 +297,14 @@ func CreatePullRequest(ctx *context.APIContext) { defer headGitRepo.Close() // Check if another PR exists with the same targets - existingPr, err := models.GetUnmergedPullRequest(headRepo.ID, ctx.Repo.Repository.ID, headBranch, baseBranch, models.PullRequestFlowGithub) + existingPr, err := issues_model.GetUnmergedPullRequest(headRepo.ID, ctx.Repo.Repository.ID, headBranch, baseBranch, issues_model.PullRequestFlowGithub) if err != nil { - if !models.IsErrPullRequestNotExist(err) { + if !issues_model.IsErrPullRequestNotExist(err) { ctx.Error(http.StatusInternalServerError, "GetUnmergedPullRequest", err) return } } else { - err = models.ErrPullRequestAlreadyExists{ + err = issues_model.ErrPullRequestAlreadyExists{ ID: existingPr.ID, IssueID: existingPr.Index, HeadRepoID: existingPr.HeadRepoID, @@ -317,7 +317,7 @@ func CreatePullRequest(ctx *context.APIContext) { } if len(form.Labels) > 0 { - labels, err := models.GetLabelsInRepoByIDs(ctx.Repo.Repository.ID, form.Labels) + labels, err := issues_model.GetLabelsInRepoByIDs(ctx.Repo.Repository.ID, form.Labels) if err != nil { ctx.Error(http.StatusInternalServerError, "GetLabelsInRepoByIDs", err) return @@ -331,7 +331,7 @@ func CreatePullRequest(ctx *context.APIContext) { } if ctx.Repo.Owner.IsOrganization() { - orgLabels, err := models.GetLabelsInOrgByIDs(ctx.Repo.Owner.ID, form.Labels) + orgLabels, err := issues_model.GetLabelsInOrgByIDs(ctx.Repo.Owner.ID, form.Labels) if err != nil { ctx.Error(http.StatusInternalServerError, "GetLabelsInOrgByIDs", err) return @@ -364,7 +364,7 @@ func CreatePullRequest(ctx *context.APIContext) { deadlineUnix = timeutil.TimeStamp(form.Deadline.Unix()) } - prIssue := &models.Issue{ + prIssue := &issues_model.Issue{ RepoID: repo.ID, Title: form.Title, PosterID: ctx.Doer.ID, @@ -374,7 +374,7 @@ func CreatePullRequest(ctx *context.APIContext) { Content: form.Body, DeadlineUnix: deadlineUnix, } - pr := &models.PullRequest{ + pr := &issues_model.PullRequest{ HeadRepoID: headRepo.ID, BaseRepoID: repo.ID, HeadBranch: headBranch, @@ -382,11 +382,11 @@ func CreatePullRequest(ctx *context.APIContext) { HeadRepo: headRepo, BaseRepo: repo, MergeBase: compareInfo.MergeBase, - Type: models.PullRequestGitea, + Type: issues_model.PullRequestGitea, } // Get all assignee IDs - assigneeIDs, err := models.MakeIDsFromAPIAssigneesToAdd(form.Assignee, form.Assignees) + assigneeIDs, err := issues_model.MakeIDsFromAPIAssigneesToAdd(form.Assignee, form.Assignees) if err != nil { if user_model.IsErrUserNotExist(err) { ctx.Error(http.StatusUnprocessableEntity, "", fmt.Sprintf("Assignee does not exist: [name: %s]", err)) @@ -409,13 +409,13 @@ func CreatePullRequest(ctx *context.APIContext) { return } if !valid { - ctx.Error(http.StatusUnprocessableEntity, "canBeAssigned", models.ErrUserDoesNotHaveAccessToRepo{UserID: aID, RepoName: repo.Name}) + ctx.Error(http.StatusUnprocessableEntity, "canBeAssigned", repo_model.ErrUserDoesNotHaveAccessToRepo{UserID: aID, RepoName: repo.Name}) return } } if err := pull_service.NewPullRequest(ctx, repo, prIssue, labelIDs, []string{}, pr, assigneeIDs); err != nil { - if models.IsErrUserDoesNotHaveAccessToRepo(err) { + if repo_model.IsErrUserDoesNotHaveAccessToRepo(err) { ctx.Error(http.StatusBadRequest, "UserDoesNotHaveAccessToRepo", err) return } @@ -470,9 +470,9 @@ func EditPullRequest(ctx *context.APIContext) { // "$ref": "#/responses/validationError" form := web.GetForm(ctx).(*api.EditPullRequestOption) - pr, err := models.GetPullRequestByIndex(ctx, ctx.Repo.Repository.ID, ctx.ParamsInt64(":index")) + pr, err := issues_model.GetPullRequestByIndex(ctx, ctx.Repo.Repository.ID, ctx.ParamsInt64(":index")) if err != nil { - if models.IsErrPullRequestNotExist(err) { + if issues_model.IsErrPullRequestNotExist(err) { ctx.NotFound() } else { ctx.Error(http.StatusInternalServerError, "GetPullRequestByIndex", err) @@ -510,7 +510,7 @@ func EditPullRequest(ctx *context.APIContext) { deadlineUnix = timeutil.TimeStamp(deadline.Unix()) } - if err := models.UpdateIssueDeadline(issue, deadlineUnix, ctx.Doer); err != nil { + if err := issues_model.UpdateIssueDeadline(issue, deadlineUnix, ctx.Doer); err != nil { ctx.Error(http.StatusInternalServerError, "UpdateIssueDeadline", err) return } @@ -548,14 +548,14 @@ func EditPullRequest(ctx *context.APIContext) { } if ctx.Repo.CanWrite(unit.TypePullRequests) && form.Labels != nil { - labels, err := models.GetLabelsInRepoByIDs(ctx.Repo.Repository.ID, form.Labels) + labels, err := issues_model.GetLabelsInRepoByIDs(ctx.Repo.Repository.ID, form.Labels) if err != nil { ctx.Error(http.StatusInternalServerError, "GetLabelsInRepoByIDsError", err) return } if ctx.Repo.Owner.IsOrganization() { - orgLabels, err := models.GetLabelsInOrgByIDs(ctx.Repo.Owner.ID, form.Labels) + orgLabels, err := issues_model.GetLabelsInOrgByIDs(ctx.Repo.Owner.ID, form.Labels) if err != nil { ctx.Error(http.StatusInternalServerError, "GetLabelsInOrgByIDs", err) return @@ -564,7 +564,7 @@ func EditPullRequest(ctx *context.APIContext) { labels = append(labels, orgLabels...) } - if err = models.ReplaceIssueLabels(issue, labels, ctx.Doer); err != nil { + if err = issues_model.ReplaceIssueLabels(issue, labels, ctx.Doer); err != nil { ctx.Error(http.StatusInternalServerError, "ReplaceLabelsError", err) return } @@ -577,9 +577,9 @@ func EditPullRequest(ctx *context.APIContext) { } issue.IsClosed = api.StateClosed == api.StateType(*form.State) } - statusChangeComment, titleChanged, err := models.UpdateIssueByAPI(issue, ctx.Doer) + statusChangeComment, titleChanged, err := issues_model.UpdateIssueByAPI(issue, ctx.Doer) if err != nil { - if models.IsErrDependenciesLeft(err) { + if issues_model.IsErrDependenciesLeft(err) { ctx.Error(http.StatusPreconditionFailed, "DependenciesLeft", "cannot close this pull request because it still has open dependencies") return } @@ -602,10 +602,10 @@ func EditPullRequest(ctx *context.APIContext) { return } if err := pull_service.ChangeTargetBranch(ctx, pr, ctx.Doer, form.Base); err != nil { - if models.IsErrPullRequestAlreadyExists(err) { + if issues_model.IsErrPullRequestAlreadyExists(err) { ctx.Error(http.StatusConflict, "IsErrPullRequestAlreadyExists", err) return - } else if models.IsErrIssueIsClosed(err) { + } else if issues_model.IsErrIssueIsClosed(err) { ctx.Error(http.StatusUnprocessableEntity, "IsErrIssueIsClosed", err) return } else if models.IsErrPullRequestHasMerged(err) { @@ -632,9 +632,9 @@ func EditPullRequest(ctx *context.APIContext) { } // Refetch from database - pr, err = models.GetPullRequestByIndex(ctx, ctx.Repo.Repository.ID, pr.Index) + pr, err = issues_model.GetPullRequestByIndex(ctx, ctx.Repo.Repository.ID, pr.Index) if err != nil { - if models.IsErrPullRequestNotExist(err) { + if issues_model.IsErrPullRequestNotExist(err) { ctx.NotFound() } else { ctx.Error(http.StatusInternalServerError, "GetPullRequestByIndex", err) @@ -676,9 +676,9 @@ func IsPullRequestMerged(ctx *context.APIContext) { // "404": // description: pull request has not been merged - pr, err := models.GetPullRequestByIndex(ctx, ctx.Repo.Repository.ID, ctx.ParamsInt64(":index")) + pr, err := issues_model.GetPullRequestByIndex(ctx, ctx.Repo.Repository.ID, ctx.ParamsInt64(":index")) if err != nil { - if models.IsErrPullRequestNotExist(err) { + if issues_model.IsErrPullRequestNotExist(err) { ctx.NotFound() } else { ctx.Error(http.StatusInternalServerError, "GetPullRequestByIndex", err) @@ -730,9 +730,9 @@ func MergePullRequest(ctx *context.APIContext) { form := web.GetForm(ctx).(*forms.MergePullRequestForm) - pr, err := models.GetPullRequestByIndex(ctx, ctx.Repo.Repository.ID, ctx.ParamsInt64(":index")) + pr, err := issues_model.GetPullRequestByIndex(ctx, ctx.Repo.Repository.ID, ctx.ParamsInt64(":index")) if err != nil { - if models.IsErrPullRequestNotExist(err) { + if issues_model.IsErrPullRequestNotExist(err) { ctx.NotFound("GetPullRequestByIndex", err) } else { ctx.Error(http.StatusInternalServerError, "GetPullRequestByIndex", err) @@ -753,7 +753,7 @@ func MergePullRequest(ctx *context.APIContext) { if ctx.IsSigned { // Update issue-user. - if err = pr.Issue.ReadBy(ctx, ctx.Doer.ID); err != nil { + if err = models.SetIssueReadBy(ctx, pr.Issue.ID, ctx.Doer.ID); err != nil { ctx.Error(http.StatusInternalServerError, "ReadBy", err) return } @@ -868,7 +868,7 @@ func MergePullRequest(ctx *context.APIContext) { if form.DeleteBranchAfterMerge { // Don't cleanup when there are other PR's that use this branch as head branch. - exist, err := models.HasUnmergedPullRequestsByHeadInfo(ctx, pr.HeadRepoID, pr.HeadBranch) + exist, err := issues_model.HasUnmergedPullRequestsByHeadInfo(ctx, pr.HeadRepoID, pr.HeadBranch) if err != nil { ctx.ServerError("HasUnmergedPullRequestsByHeadInfo", err) return @@ -902,7 +902,7 @@ func MergePullRequest(ctx *context.APIContext) { } return } - if err := models.AddDeletePRBranchComment(ctx, ctx.Doer, pr.BaseRepo, pr.Issue.ID, pr.HeadBranch); err != nil { + if err := issues_model.AddDeletePRBranchComment(ctx, ctx.Doer, pr.BaseRepo, pr.Issue.ID, pr.HeadBranch); err != nil { // Do not fail here as branch has already been deleted log.Error("DeleteBranch: %v", err) } @@ -1079,9 +1079,9 @@ func UpdatePullRequest(ctx *context.APIContext) { // "422": // "$ref": "#/responses/validationError" - pr, err := models.GetPullRequestByIndex(ctx, ctx.Repo.Repository.ID, ctx.ParamsInt64(":index")) + pr, err := issues_model.GetPullRequestByIndex(ctx, ctx.Repo.Repository.ID, ctx.ParamsInt64(":index")) if err != nil { - if models.IsErrPullRequestNotExist(err) { + if issues_model.IsErrPullRequestNotExist(err) { ctx.NotFound() } else { ctx.Error(http.StatusInternalServerError, "GetPullRequestByIndex", err) @@ -1177,9 +1177,9 @@ func CancelScheduledAutoMerge(ctx *context.APIContext) { // "$ref": "#/responses/notFound" pullIndex := ctx.ParamsInt64(":index") - pull, err := models.GetPullRequestByIndex(ctx, ctx.Repo.Repository.ID, pullIndex) + pull, err := issues_model.GetPullRequestByIndex(ctx, ctx.Repo.Repository.ID, pullIndex) if err != nil { - if models.IsErrPullRequestNotExist(err) { + if issues_model.IsErrPullRequestNotExist(err) { ctx.NotFound() return } @@ -1254,9 +1254,9 @@ func GetPullRequestCommits(ctx *context.APIContext) { // "404": // "$ref": "#/responses/notFound" - pr, err := models.GetPullRequestByIndex(ctx, ctx.Repo.Repository.ID, ctx.ParamsInt64(":index")) + pr, err := issues_model.GetPullRequestByIndex(ctx, ctx.Repo.Repository.ID, ctx.ParamsInt64(":index")) if err != nil { - if models.IsErrPullRequestNotExist(err) { + if issues_model.IsErrPullRequestNotExist(err) { ctx.NotFound() } else { ctx.Error(http.StatusInternalServerError, "GetPullRequestByIndex", err) diff --git a/routers/api/v1/repo/pull_review.go b/routers/api/v1/repo/pull_review.go index 5175fa921f..57548f2102 100644 --- a/routers/api/v1/repo/pull_review.go +++ b/routers/api/v1/repo/pull_review.go @@ -9,7 +9,7 @@ import ( "net/http" "strings" - "code.gitea.io/gitea/models" + issues_model "code.gitea.io/gitea/models/issues" "code.gitea.io/gitea/models/organization" access_model "code.gitea.io/gitea/models/perm/access" user_model "code.gitea.io/gitea/models/user" @@ -61,9 +61,9 @@ func ListPullReviews(ctx *context.APIContext) { // "404": // "$ref": "#/responses/notFound" - pr, err := models.GetPullRequestByIndex(ctx, ctx.Repo.Repository.ID, ctx.ParamsInt64(":index")) + pr, err := issues_model.GetPullRequestByIndex(ctx, ctx.Repo.Repository.ID, ctx.ParamsInt64(":index")) if err != nil { - if models.IsErrPullRequestNotExist(err) { + if issues_model.IsErrPullRequestNotExist(err) { ctx.NotFound("GetPullRequestByIndex", err) } else { ctx.Error(http.StatusInternalServerError, "GetPullRequestByIndex", err) @@ -81,19 +81,19 @@ func ListPullReviews(ctx *context.APIContext) { return } - opts := models.FindReviewOptions{ + opts := issues_model.FindReviewOptions{ ListOptions: utils.GetListOptions(ctx), - Type: models.ReviewTypeUnknown, + Type: issues_model.ReviewTypeUnknown, IssueID: pr.IssueID, } - allReviews, err := models.FindReviews(ctx, opts) + allReviews, err := issues_model.FindReviews(ctx, opts) if err != nil { ctx.InternalServerError(err) return } - count, err := models.CountReviews(opts) + count, err := issues_model.CountReviews(opts) if err != nil { ctx.InternalServerError(err) return @@ -261,7 +261,7 @@ func DeletePullReview(ctx *context.APIContext) { return } - if err := models.DeleteReview(review); err != nil { + if err := issues_model.DeleteReview(review); err != nil { ctx.Error(http.StatusInternalServerError, "DeleteReview", fmt.Errorf("can not delete ReviewID: %d", review.ID)) return } @@ -307,9 +307,9 @@ func CreatePullReview(ctx *context.APIContext) { // "$ref": "#/responses/validationError" opts := web.GetForm(ctx).(*api.CreatePullReviewOptions) - pr, err := models.GetPullRequestByIndex(ctx, ctx.Repo.Repository.ID, ctx.ParamsInt64(":index")) + pr, err := issues_model.GetPullRequestByIndex(ctx, ctx.Repo.Repository.ID, ctx.ParamsInt64(":index")) if err != nil { - if models.IsErrPullRequestNotExist(err) { + if issues_model.IsErrPullRequestNotExist(err) { ctx.NotFound("GetPullRequestByIndex", err) } else { ctx.Error(http.StatusInternalServerError, "GetPullRequestByIndex", err) @@ -435,7 +435,7 @@ func SubmitPullReview(ctx *context.APIContext) { return } - if review.Type != models.ReviewTypePending { + if review.Type != issues_model.ReviewTypePending { ctx.Error(http.StatusUnprocessableEntity, "", fmt.Errorf("only a pending review can be submitted")) return } @@ -447,7 +447,7 @@ func SubmitPullReview(ctx *context.APIContext) { } // if review stay pending return - if reviewType == models.ReviewTypePending { + if reviewType == issues_model.ReviewTypePending { ctx.Error(http.StatusUnprocessableEntity, "", fmt.Errorf("review stay pending")) return } @@ -475,7 +475,7 @@ func SubmitPullReview(ctx *context.APIContext) { } // preparePullReviewType return ReviewType and false or nil and true if an error happen -func preparePullReviewType(ctx *context.APIContext, pr *models.PullRequest, event api.ReviewStateType, body string, hasComments bool) (models.ReviewType, bool) { +func preparePullReviewType(ctx *context.APIContext, pr *issues_model.PullRequest, event api.ReviewStateType, body string, hasComments bool) (issues_model.ReviewType, bool) { if err := pr.LoadIssue(); err != nil { ctx.Error(http.StatusInternalServerError, "LoadIssue", err) return -1, true @@ -484,7 +484,7 @@ func preparePullReviewType(ctx *context.APIContext, pr *models.PullRequest, even needsBody := true hasBody := len(strings.TrimSpace(body)) > 0 - var reviewType models.ReviewType + var reviewType issues_model.ReviewType switch event { case api.ReviewStateApproved: // can not approve your own PR @@ -492,7 +492,7 @@ func preparePullReviewType(ctx *context.APIContext, pr *models.PullRequest, even ctx.Error(http.StatusUnprocessableEntity, "", fmt.Errorf("approve your own pull is not allowed")) return -1, true } - reviewType = models.ReviewTypeApprove + reviewType = issues_model.ReviewTypeApprove needsBody = false case api.ReviewStateRequestChanges: @@ -501,10 +501,10 @@ func preparePullReviewType(ctx *context.APIContext, pr *models.PullRequest, even ctx.Error(http.StatusUnprocessableEntity, "", fmt.Errorf("reject your own pull is not allowed")) return -1, true } - reviewType = models.ReviewTypeReject + reviewType = issues_model.ReviewTypeReject case api.ReviewStateComment: - reviewType = models.ReviewTypeComment + reviewType = issues_model.ReviewTypeComment needsBody = false // if there is no body we need to ensure that there are comments if !hasBody && !hasComments { @@ -512,7 +512,7 @@ func preparePullReviewType(ctx *context.APIContext, pr *models.PullRequest, even return -1, true } default: - reviewType = models.ReviewTypePending + reviewType = issues_model.ReviewTypePending } // reject reviews with empty body if a body is required for this call @@ -525,10 +525,10 @@ func preparePullReviewType(ctx *context.APIContext, pr *models.PullRequest, even } // prepareSingleReview return review, related pull and false or nil, nil and true if an error happen -func prepareSingleReview(ctx *context.APIContext) (*models.Review, *models.PullRequest, bool) { - pr, err := models.GetPullRequestByIndex(ctx, ctx.Repo.Repository.ID, ctx.ParamsInt64(":index")) +func prepareSingleReview(ctx *context.APIContext) (*issues_model.Review, *issues_model.PullRequest, bool) { + pr, err := issues_model.GetPullRequestByIndex(ctx, ctx.Repo.Repository.ID, ctx.ParamsInt64(":index")) if err != nil { - if models.IsErrPullRequestNotExist(err) { + if issues_model.IsErrPullRequestNotExist(err) { ctx.NotFound("GetPullRequestByIndex", err) } else { ctx.Error(http.StatusInternalServerError, "GetPullRequestByIndex", err) @@ -536,9 +536,9 @@ func prepareSingleReview(ctx *context.APIContext) (*models.Review, *models.PullR return nil, nil, true } - review, err := models.GetReviewByID(ctx, ctx.ParamsInt64(":id")) + review, err := issues_model.GetReviewByID(ctx, ctx.ParamsInt64(":id")) if err != nil { - if models.IsErrReviewNotExist(err) { + if issues_model.IsErrReviewNotExist(err) { ctx.NotFound("GetReviewByID", err) } else { ctx.Error(http.StatusInternalServerError, "GetReviewByID", err) @@ -553,7 +553,7 @@ func prepareSingleReview(ctx *context.APIContext) (*models.Review, *models.PullR } // make sure that the user has access to this review if it is pending - if review.Type == models.ReviewTypePending && review.ReviewerID != ctx.Doer.ID && !ctx.Doer.IsAdmin { + if review.Type == issues_model.ReviewTypePending && review.ReviewerID != ctx.Doer.ID && !ctx.Doer.IsAdmin { ctx.NotFound("GetReviewByID") return nil, nil, true } @@ -648,9 +648,9 @@ func DeleteReviewRequests(ctx *context.APIContext) { } func apiReviewRequest(ctx *context.APIContext, opts api.PullReviewRequestOptions, isAdd bool) { - pr, err := models.GetPullRequestByIndex(ctx, ctx.Repo.Repository.ID, ctx.ParamsInt64(":index")) + pr, err := issues_model.GetPullRequestByIndex(ctx, ctx.Repo.Repository.ID, ctx.ParamsInt64(":index")) if err != nil { - if models.IsErrPullRequestNotExist(err) { + if issues_model.IsErrPullRequestNotExist(err) { ctx.NotFound("GetPullRequestByIndex", err) } else { ctx.Error(http.StatusInternalServerError, "GetPullRequestByIndex", err) @@ -690,7 +690,7 @@ func apiReviewRequest(ctx *context.APIContext, opts api.PullReviewRequestOptions err = issue_service.IsValidReviewRequest(ctx, reviewer, ctx.Doer, isAdd, pr.Issue, &permDoer) if err != nil { - if models.IsErrNotValidReviewRequest(err) { + if issues_model.IsErrNotValidReviewRequest(err) { ctx.Error(http.StatusUnprocessableEntity, "NotValidReviewRequest", err) return } @@ -701,9 +701,9 @@ func apiReviewRequest(ctx *context.APIContext, opts api.PullReviewRequestOptions reviewers = append(reviewers, reviewer) } - var reviews []*models.Review + var reviews []*issues_model.Review if isAdd { - reviews = make([]*models.Review, 0, len(reviewers)) + reviews = make([]*issues_model.Review, 0, len(reviewers)) } for _, reviewer := range reviewers { @@ -739,7 +739,7 @@ func apiReviewRequest(ctx *context.APIContext, opts api.PullReviewRequestOptions err = issue_service.IsValidTeamReviewRequest(ctx, teamReviewer, ctx.Doer, isAdd, pr.Issue) if err != nil { - if models.IsErrNotValidReviewRequest(err) { + if issues_model.IsErrNotValidReviewRequest(err) { ctx.Error(http.StatusUnprocessableEntity, "NotValidReviewRequest", err) return } @@ -876,7 +876,7 @@ func dismissReview(ctx *context.APIContext, msg string, isDismiss bool) { return } - if review.Type != models.ReviewTypeApprove && review.Type != models.ReviewTypeReject { + if review.Type != issues_model.ReviewTypeApprove && review.Type != issues_model.ReviewTypeReject { ctx.Error(http.StatusForbidden, "", "not need to dismiss this review because it's type is not Approve or change request") return } @@ -892,7 +892,7 @@ func dismissReview(ctx *context.APIContext, msg string, isDismiss bool) { return } - if review, err = models.GetReviewByID(ctx, review.ID); err != nil { + if review, err = issues_model.GetReviewByID(ctx, review.ID); err != nil { ctx.Error(http.StatusInternalServerError, "GetReviewByID", err) return } diff --git a/routers/private/hook_post_receive.go b/routers/private/hook_post_receive.go index eb2bbc1e5f..93aa450f9c 100644 --- a/routers/private/hook_post_receive.go +++ b/routers/private/hook_post_receive.go @@ -11,7 +11,7 @@ import ( "strconv" "strings" - "code.gitea.io/gitea/models" + issues_model "code.gitea.io/gitea/models/issues" repo_model "code.gitea.io/gitea/models/repo" gitea_context "code.gitea.io/gitea/modules/context" "code.gitea.io/gitea/modules/git" @@ -141,8 +141,8 @@ func HookPostReceive(ctx *gitea_context.PrivateContext) { continue } - pr, err := models.GetPullRequestByIndex(ctx, repo.ID, pullIndex) - if err != nil && !models.IsErrPullRequestNotExist(err) { + pr, err := issues_model.GetPullRequestByIndex(ctx, repo.ID, pullIndex) + if err != nil && !issues_model.IsErrPullRequestNotExist(err) { log.Error("Failed to get PR by index %v Error: %v", pullIndex, err) ctx.JSON(http.StatusInternalServerError, private.Response{ Err: fmt.Sprintf("Failed to get PR by index %v Error: %v", pullIndex, err), @@ -202,8 +202,8 @@ func HookPostReceive(ctx *gitea_context.PrivateContext) { continue } - pr, err := models.GetUnmergedPullRequest(repo.ID, baseRepo.ID, branch, baseRepo.DefaultBranch, models.PullRequestFlowGithub) - if err != nil && !models.IsErrPullRequestNotExist(err) { + pr, err := issues_model.GetUnmergedPullRequest(repo.ID, baseRepo.ID, branch, baseRepo.DefaultBranch, issues_model.PullRequestFlowGithub) + if err != nil && !issues_model.IsErrPullRequestNotExist(err) { log.Error("Failed to get active PR in: %-v Branch: %s to: %-v Branch: %s Error: %v", repo, branch, baseRepo, baseRepo.DefaultBranch, err) ctx.JSON(http.StatusInternalServerError, private.HookPostReceiveResult{ Err: fmt.Sprintf( diff --git a/routers/private/hook_pre_receive.go b/routers/private/hook_pre_receive.go index 411319f2e6..cadfea782c 100644 --- a/routers/private/hook_pre_receive.go +++ b/routers/private/hook_pre_receive.go @@ -14,6 +14,7 @@ import ( "code.gitea.io/gitea/models" asymkey_model "code.gitea.io/gitea/models/asymkey" git_model "code.gitea.io/gitea/models/git" + issues_model "code.gitea.io/gitea/models/issues" perm_model "code.gitea.io/gitea/models/perm" access_model "code.gitea.io/gitea/models/perm/access" "code.gitea.io/gitea/models/unit" @@ -57,7 +58,7 @@ func (ctx *preReceiveContext) CanWriteCode() bool { if !ctx.loadPusherAndPermission() { return false } - ctx.canWriteCode = models.CanMaintainerWriteToBranch(ctx.userPerm, ctx.branchName, ctx.user) || ctx.deployKeyAccessMode >= perm_model.AccessModeWrite + ctx.canWriteCode = issues_model.CanMaintainerWriteToBranch(ctx.userPerm, ctx.branchName, ctx.user) || ctx.deployKeyAccessMode >= perm_model.AccessModeWrite ctx.checkedCanWriteCode = true } return ctx.canWriteCode @@ -296,7 +297,7 @@ func preReceiveBranch(ctx *preReceiveContext, oldCommitID, newCommitID, refFullN // 6b. Merge (from UI or API) // Get the PR, user and permissions for the user in the repository - pr, err := models.GetPullRequestByID(ctx, ctx.opts.PullRequestID) + pr, err := issues_model.GetPullRequestByID(ctx, ctx.opts.PullRequestID) if err != nil { log.Error("Unable to get PullRequest %d Error: %v", ctx.opts.PullRequestID, err) ctx.JSON(http.StatusInternalServerError, private.Response{ diff --git a/routers/web/org/org_labels.go b/routers/web/org/org_labels.go index bfa9f162c3..185e1eee31 100644 --- a/routers/web/org/org_labels.go +++ b/routers/web/org/org_labels.go @@ -7,8 +7,8 @@ package org import ( "net/http" - "code.gitea.io/gitea/models" "code.gitea.io/gitea/models/db" + issues_model "code.gitea.io/gitea/models/issues" "code.gitea.io/gitea/modules/context" repo_module "code.gitea.io/gitea/modules/repository" "code.gitea.io/gitea/modules/web" @@ -17,7 +17,7 @@ import ( // RetrieveLabels find all the labels of an organization func RetrieveLabels(ctx *context.Context) { - labels, err := models.GetLabelsByOrgID(ctx, ctx.Org.Organization.ID, ctx.FormString("sort"), db.ListOptions{}) + labels, err := issues_model.GetLabelsByOrgID(ctx, ctx.Org.Organization.ID, ctx.FormString("sort"), db.ListOptions{}) if err != nil { ctx.ServerError("RetrieveLabels.GetLabels", err) return @@ -43,13 +43,13 @@ func NewLabel(ctx *context.Context) { return } - l := &models.Label{ + l := &issues_model.Label{ OrgID: ctx.Org.Organization.ID, Name: form.Title, Description: form.Description, Color: form.Color, } - if err := models.NewLabel(ctx, l); err != nil { + if err := issues_model.NewLabel(ctx, l); err != nil { ctx.ServerError("NewLabel", err) return } @@ -59,10 +59,10 @@ func NewLabel(ctx *context.Context) { // UpdateLabel update a label's name and color func UpdateLabel(ctx *context.Context) { form := web.GetForm(ctx).(*forms.CreateLabelForm) - l, err := models.GetLabelInOrgByID(ctx, ctx.Org.Organization.ID, form.ID) + l, err := issues_model.GetLabelInOrgByID(ctx, ctx.Org.Organization.ID, form.ID) if err != nil { switch { - case models.IsErrOrgLabelNotExist(err): + case issues_model.IsErrOrgLabelNotExist(err): ctx.Error(http.StatusNotFound) default: ctx.ServerError("UpdateLabel", err) @@ -73,7 +73,7 @@ func UpdateLabel(ctx *context.Context) { l.Name = form.Title l.Description = form.Description l.Color = form.Color - if err := models.UpdateLabel(l); err != nil { + if err := issues_model.UpdateLabel(l); err != nil { ctx.ServerError("UpdateLabel", err) return } @@ -82,7 +82,7 @@ func UpdateLabel(ctx *context.Context) { // DeleteLabel delete a label func DeleteLabel(ctx *context.Context) { - if err := models.DeleteLabel(ctx.Org.Organization.ID, ctx.FormInt64("id")); err != nil { + if err := issues_model.DeleteLabel(ctx.Org.Organization.ID, ctx.FormInt64("id")); err != nil { ctx.Flash.Error("DeleteLabel: " + err.Error()) } else { ctx.Flash.Success(ctx.Tr("repo.issues.label_deletion_success")) diff --git a/routers/web/repo/branch.go b/routers/web/repo/branch.go index 84ad803ee5..4bd2af4e8e 100644 --- a/routers/web/repo/branch.go +++ b/routers/web/repo/branch.go @@ -13,6 +13,7 @@ import ( "code.gitea.io/gitea/models" 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/unit" "code.gitea.io/gitea/modules/base" @@ -44,7 +45,7 @@ type Branch struct { DeletedBranch *git_model.DeletedBranch CommitsAhead int CommitsBehind int - LatestPullRequest *models.PullRequest + LatestPullRequest *issues_model.PullRequest MergeMovedOn bool } @@ -264,7 +265,7 @@ func loadOneBranch(ctx *context.Context, rawBranch, defaultBranch *git.Branch, p } } - pr, err := models.GetLatestPullRequestByHeadInfo(ctx.Repo.Repository.ID, branchName) + pr, err := issues_model.GetLatestPullRequestByHeadInfo(ctx.Repo.Repository.ID, branchName) if err != nil { ctx.ServerError("GetLatestPullRequestByHeadInfo", err) return nil diff --git a/routers/web/repo/compare.go b/routers/web/repo/compare.go index 44e89062e8..605594d5a9 100644 --- a/routers/web/repo/compare.go +++ b/routers/web/repo/compare.go @@ -19,6 +19,7 @@ import ( "code.gitea.io/gitea/models" git_model "code.gitea.io/gitea/models/git" + issues_model "code.gitea.io/gitea/models/issues" access_model "code.gitea.io/gitea/models/perm/access" repo_model "code.gitea.io/gitea/models/repo" "code.gitea.io/gitea/models/unit" @@ -747,9 +748,9 @@ func CompareDiff(ctx *context.Context) { ctx.Data["HeadTags"] = headTags if ctx.Data["PageIsComparePull"] == true { - pr, err := models.GetUnmergedPullRequest(ci.HeadRepo.ID, ctx.Repo.Repository.ID, ci.HeadBranch, ci.BaseBranch, models.PullRequestFlowGithub) + pr, err := issues_model.GetUnmergedPullRequest(ci.HeadRepo.ID, ctx.Repo.Repository.ID, ci.HeadBranch, ci.BaseBranch, issues_model.PullRequestFlowGithub) if err != nil { - if !models.IsErrPullRequestNotExist(err) { + if !issues_model.IsErrPullRequestNotExist(err) { ctx.ServerError("GetUnmergedPullRequest", err) return } diff --git a/routers/web/repo/issue.go b/routers/web/repo/issue.go index 38ee933044..11d2daeeff 100644 --- a/routers/web/repo/issue.go +++ b/routers/web/repo/issue.go @@ -183,11 +183,11 @@ func issues(ctx *context.Context, milestoneID, projectID int64, isPullOption uti } } - var issueStats *models.IssueStats + var issueStats *issues_model.IssueStats if forceEmpty { - issueStats = &models.IssueStats{} + issueStats = &issues_model.IssueStats{} } else { - issueStats, err = models.GetIssueStats(&models.IssueStatsOptions{ + issueStats, err = issues_model.GetIssueStats(&issues_model.IssueStatsOptions{ RepoID: repo.ID, Labels: selectLabels, MilestoneID: milestoneID, @@ -228,11 +228,11 @@ func issues(ctx *context.Context, milestoneID, projectID int64, isPullOption uti mileIDs = []int64{milestoneID} } - var issues []*models.Issue + var issues []*issues_model.Issue if forceEmpty { - issues = []*models.Issue{} + issues = []*issues_model.Issue{} } else { - issues, err = models.Issues(&models.IssuesOptions{ + issues, err = issues_model.Issues(&issues_model.IssuesOptions{ ListOptions: db.ListOptions{ Page: pager.Paginater.Current(), PageSize: setting.UI.IssuePagingNum, @@ -256,7 +256,7 @@ func issues(ctx *context.Context, milestoneID, projectID int64, isPullOption uti } } - issueList := models.IssueList(issues) + issueList := issues_model.IssueList(issues) approvalCounts, err := issueList.GetApprovalCounts(ctx) if err != nil { ctx.ServerError("ApprovalCounts", err) @@ -296,14 +296,14 @@ func issues(ctx *context.Context, milestoneID, projectID int64, isPullOption uti return } - labels, err := models.GetLabelsByRepoID(ctx, repo.ID, "", db.ListOptions{}) + labels, err := issues_model.GetLabelsByRepoID(ctx, repo.ID, "", db.ListOptions{}) if err != nil { ctx.ServerError("GetLabelsByRepoID", err) return } if repo.Owner.IsOrganization() { - orgLabels, err := models.GetLabelsByOrgID(ctx, repo.Owner.ID, ctx.FormString("sort"), db.ListOptions{}) + orgLabels, err := issues_model.GetLabelsByOrgID(ctx, repo.Owner.ID, ctx.FormString("sort"), db.ListOptions{}) if err != nil { ctx.ServerError("GetLabelsByOrgID", err) return @@ -330,11 +330,11 @@ func issues(ctx *context.Context, milestoneID, projectID int64, isPullOption uti if !ok || len(counts) == 0 { return 0 } - reviewTyp := models.ReviewTypeApprove + reviewTyp := issues_model.ReviewTypeApprove if typ == "reject" { - reviewTyp = models.ReviewTypeReject + reviewTyp = issues_model.ReviewTypeReject } else if typ == "waiting" { - reviewTyp = models.ReviewTypeRequest + reviewTyp = issues_model.ReviewTypeRequest } for _, count := range counts { if count.Type == reviewTyp { @@ -483,24 +483,24 @@ type repoReviewerSelection struct { IsTeam bool Team *organization.Team User *user_model.User - Review *models.Review + Review *issues_model.Review CanChange bool Checked bool ItemID int64 } // RetrieveRepoReviewers find all reviewers of a repository -func RetrieveRepoReviewers(ctx *context.Context, repo *repo_model.Repository, issue *models.Issue, canChooseReviewer bool) { +func RetrieveRepoReviewers(ctx *context.Context, repo *repo_model.Repository, issue *issues_model.Issue, canChooseReviewer bool) { ctx.Data["CanChooseReviewer"] = canChooseReviewer - originalAuthorReviews, err := models.GetReviewersFromOriginalAuthorsByIssueID(issue.ID) + originalAuthorReviews, err := issues_model.GetReviewersFromOriginalAuthorsByIssueID(issue.ID) if err != nil { ctx.ServerError("GetReviewersFromOriginalAuthorsByIssueID", err) return } ctx.Data["OriginalReviews"] = originalAuthorReviews - reviews, err := models.GetReviewersByIssueID(issue.ID) + reviews, err := issues_model.GetReviewersByIssueID(issue.ID) if err != nil { ctx.ServerError("GetReviewersByIssueID", err) return @@ -549,7 +549,7 @@ func RetrieveRepoReviewers(ctx *context.Context, repo *repo_model.Repository, is for _, review := range reviews { tmp := &repoReviewerSelection{ - Checked: review.Type == models.ReviewTypeRequest, + Checked: review.Type == issues_model.ReviewTypeRequest, Review: review, ItemID: review.ReviewerID, } @@ -561,10 +561,10 @@ func RetrieveRepoReviewers(ctx *context.Context, repo *repo_model.Repository, is if ctx.Repo.IsAdmin() { // Admin can dismiss or re-request any review requests tmp.CanChange = true - } else if ctx.Doer != nil && ctx.Doer.ID == review.ReviewerID && review.Type == models.ReviewTypeRequest { + } else if ctx.Doer != nil && ctx.Doer.ID == review.ReviewerID && review.Type == issues_model.ReviewTypeRequest { // A user can refuse review requests tmp.CanChange = true - } else if (canChooseReviewer || (ctx.Doer != nil && ctx.Doer.ID == issue.PosterID)) && review.Type != models.ReviewTypeRequest && + } else if (canChooseReviewer || (ctx.Doer != nil && ctx.Doer.ID == issue.PosterID)) && review.Type != issues_model.ReviewTypeRequest && ctx.Doer.ID != review.ReviewerID { // The poster of the PR, a manager, or official reviewers can re-request review from other reviewers tmp.CanChange = true @@ -670,19 +670,19 @@ func RetrieveRepoReviewers(ctx *context.Context, repo *repo_model.Repository, is } // RetrieveRepoMetas find all the meta information of a repository -func RetrieveRepoMetas(ctx *context.Context, repo *repo_model.Repository, isPull bool) []*models.Label { +func RetrieveRepoMetas(ctx *context.Context, repo *repo_model.Repository, isPull bool) []*issues_model.Label { if !ctx.Repo.CanWriteIssuesOrPulls(isPull) { return nil } - labels, err := models.GetLabelsByRepoID(ctx, repo.ID, "", db.ListOptions{}) + labels, err := issues_model.GetLabelsByRepoID(ctx, repo.ID, "", db.ListOptions{}) if err != nil { ctx.ServerError("GetLabelsByRepoID", err) return nil } ctx.Data["Labels"] = labels if repo.Owner.IsOrganization() { - orgLabels, err := models.GetLabelsByOrgID(ctx, repo.Owner.ID, ctx.FormString("sort"), db.ListOptions{}) + orgLabels, err := issues_model.GetLabelsByOrgID(ctx, repo.Owner.ID, ctx.FormString("sort"), db.ListOptions{}) if err != nil { return nil } @@ -763,10 +763,10 @@ func setTemplateIfExists(ctx *context.Context, ctxDataKey string, possibleDirs, ctx.Data[issueTemplateTitleKey] = meta.Title ctx.Data[ctxDataKey] = templateBody labelIDs := make([]string, 0, len(meta.Labels)) - if repoLabels, err := models.GetLabelsByRepoID(ctx, ctx.Repo.Repository.ID, "", db.ListOptions{}); err == nil { + if repoLabels, err := issues_model.GetLabelsByRepoID(ctx, ctx.Repo.Repository.ID, "", db.ListOptions{}); err == nil { ctx.Data["Labels"] = repoLabels if ctx.Repo.Owner.IsOrganization() { - if orgLabels, err := models.GetLabelsByOrgID(ctx, ctx.Repo.Owner.ID, ctx.FormString("sort"), db.ListOptions{}); err == nil { + if orgLabels, err := issues_model.GetLabelsByOrgID(ctx, ctx.Repo.Owner.ID, ctx.FormString("sort"), db.ListOptions{}); err == nil { ctx.Data["OrgLabels"] = orgLabels repoLabels = append(repoLabels, orgLabels...) } @@ -969,7 +969,7 @@ func ValidateRepoMetas(ctx *context.Context, form forms.CreateIssueForm, isPull } if !valid { - ctx.ServerError("canBeAssigned", models.ErrUserDoesNotHaveAccessToRepo{UserID: aID, RepoName: repo.Name}) + ctx.ServerError("canBeAssigned", repo_model.ErrUserDoesNotHaveAccessToRepo{UserID: aID, RepoName: repo.Name}) return nil, nil, 0, 0 } } @@ -1017,7 +1017,7 @@ func NewIssuePost(ctx *context.Context) { return } - issue := &models.Issue{ + issue := &issues_model.Issue{ RepoID: repo.ID, Repo: repo, Title: form.Title, @@ -1029,7 +1029,7 @@ func NewIssuePost(ctx *context.Context) { } if err := issue_service.NewIssue(repo, issue, labelIDs, attachments, assigneeIDs); err != nil { - if models.IsErrUserDoesNotHaveAccessToRepo(err) { + if repo_model.IsErrUserDoesNotHaveAccessToRepo(err) { ctx.Error(http.StatusBadRequest, "UserDoesNotHaveAccessToRepo", err.Error()) return } @@ -1038,7 +1038,7 @@ func NewIssuePost(ctx *context.Context) { } if projectID > 0 { - if err := models.ChangeProjectAssign(issue, ctx.Doer, projectID); err != nil { + if err := issues_model.ChangeProjectAssign(issue, ctx.Doer, projectID); err != nil { ctx.ServerError("ChangeProjectAssign", err) return } @@ -1053,29 +1053,29 @@ func NewIssuePost(ctx *context.Context) { } // roleDescriptor returns the Role Descriptor for a comment in/with the given repo, poster and issue -func roleDescriptor(ctx stdCtx.Context, repo *repo_model.Repository, poster *user_model.User, issue *models.Issue) (models.RoleDescriptor, error) { +func roleDescriptor(ctx stdCtx.Context, repo *repo_model.Repository, poster *user_model.User, issue *issues_model.Issue) (issues_model.RoleDescriptor, error) { perm, err := access_model.GetUserRepoPermission(ctx, repo, poster) if err != nil { - return models.RoleDescriptorNone, err + return issues_model.RoleDescriptorNone, err } // By default the poster has no roles on the comment. - roleDescriptor := models.RoleDescriptorNone + roleDescriptor := issues_model.RoleDescriptorNone // Check if the poster is owner of the repo. if perm.IsOwner() { // If the poster isn't a admin, enable the owner role. if !poster.IsAdmin { - roleDescriptor = roleDescriptor.WithRole(models.RoleDescriptorOwner) + roleDescriptor = roleDescriptor.WithRole(issues_model.RoleDescriptorOwner) } else { // Otherwise check if poster is the real repo admin. ok, err := access_model.IsUserRealRepoAdmin(repo, poster) if err != nil { - return models.RoleDescriptorNone, err + return issues_model.RoleDescriptorNone, err } if ok { - roleDescriptor = roleDescriptor.WithRole(models.RoleDescriptorOwner) + roleDescriptor = roleDescriptor.WithRole(issues_model.RoleDescriptorOwner) } } } @@ -1083,18 +1083,18 @@ func roleDescriptor(ctx stdCtx.Context, repo *repo_model.Repository, poster *use // Is the poster can write issues or pulls to the repo, enable the Writer role. // Only enable this if the poster doesn't have the owner role already. if !roleDescriptor.HasRole("Owner") && perm.CanWriteIssuesOrPulls(issue.IsPull) { - roleDescriptor = roleDescriptor.WithRole(models.RoleDescriptorWriter) + roleDescriptor = roleDescriptor.WithRole(issues_model.RoleDescriptorWriter) } // If the poster is the actual poster of the issue, enable Poster role. if issue.IsPoster(poster.ID) { - roleDescriptor = roleDescriptor.WithRole(models.RoleDescriptorPoster) + roleDescriptor = roleDescriptor.WithRole(issues_model.RoleDescriptorPoster) } return roleDescriptor, nil } -func getBranchData(ctx *context.Context, issue *models.Issue) { +func getBranchData(ctx *context.Context, issue *issues_model.Issue) { ctx.Data["BaseBranch"] = nil ctx.Data["HeadBranch"] = nil ctx.Data["HeadUserName"] = nil @@ -1131,9 +1131,9 @@ func ViewIssue(ctx *context.Context) { } } - issue, err := models.GetIssueByIndex(ctx.Repo.Repository.ID, ctx.ParamsInt64(":index")) + issue, err := issues_model.GetIssueByIndex(ctx.Repo.Repository.ID, ctx.ParamsInt64(":index")) if err != nil { - if models.IsErrIssueNotExist(err) { + if issues_model.IsErrIssueNotExist(err) { ctx.NotFound("GetIssueByIndex", err) } else { ctx.ServerError("GetIssueByIndex", err) @@ -1182,7 +1182,7 @@ func ViewIssue(ctx *context.Context) { ctx.Data["IsAttachmentEnabled"] = setting.Attachment.Enabled upload.AddUploadContext(ctx, "comment") - if err = issue.LoadAttributes(); err != nil { + if err = issue.LoadAttributes(ctx); err != nil { ctx.ServerError("LoadAttributes", err) return } @@ -1194,11 +1194,11 @@ func ViewIssue(ctx *context.Context) { ctx.Data["Title"] = fmt.Sprintf("#%d - %s", issue.Index, issue.Title) - iw := new(models.IssueWatch) + iw := new(issues_model.IssueWatch) if ctx.Doer != nil { iw.UserID = ctx.Doer.ID iw.IssueID = issue.ID - iw.IsWatching, err = models.CheckIssueWatch(ctx.Doer, issue) + iw.IsWatching, err = issues_model.CheckIssueWatch(ctx.Doer, issue) if err != nil { ctx.ServerError("CheckIssueWatch", err) return @@ -1239,7 +1239,7 @@ func ViewIssue(ctx *context.Context) { for i := range issue.Labels { labelIDMark[issue.Labels[i].ID] = true } - labels, err := models.GetLabelsByRepoID(ctx, repo.ID, "", db.ListOptions{}) + labels, err := issues_model.GetLabelsByRepoID(ctx, repo.ID, "", db.ListOptions{}) if err != nil { ctx.ServerError("GetLabelsByRepoID", err) return @@ -1247,7 +1247,7 @@ func ViewIssue(ctx *context.Context) { ctx.Data["Labels"] = labels if repo.Owner.IsOrganization() { - orgLabels, err := models.GetLabelsByOrgID(ctx, repo.Owner.ID, ctx.FormString("sort"), db.ListOptions{}) + orgLabels, err := issues_model.GetLabelsByOrgID(ctx, repo.Owner.ID, ctx.FormString("sort"), db.ListOptions{}) if err != nil { ctx.ServerError("GetLabelsByOrgID", err) return @@ -1279,7 +1279,7 @@ func ViewIssue(ctx *context.Context) { if issue.IsPull { canChooseReviewer := ctx.Repo.CanWrite(unit.TypePullRequests) if !canChooseReviewer && ctx.Doer != nil && ctx.IsSigned { - canChooseReviewer, err = models.IsOfficialReviewer(ctx, issue, ctx.Doer) + canChooseReviewer, err = issues_model.IsOfficialReviewer(ctx, issue, ctx.Doer) if err != nil { ctx.ServerError("IsOfficialReviewer", err) return @@ -1294,35 +1294,35 @@ func ViewIssue(ctx *context.Context) { if ctx.IsSigned { // Update issue-user. - if err = issue.ReadBy(ctx, ctx.Doer.ID); err != nil { + if err = models.SetIssueReadBy(ctx, issue.ID, ctx.Doer.ID); err != nil { ctx.ServerError("ReadBy", err) return } } var ( - role models.RoleDescriptor + role issues_model.RoleDescriptor ok bool - marked = make(map[int64]models.RoleDescriptor) - comment *models.Comment + marked = make(map[int64]issues_model.RoleDescriptor) + comment *issues_model.Comment participants = make([]*user_model.User, 1, 10) ) if ctx.Repo.Repository.IsTimetrackerEnabled() { if ctx.IsSigned { // Deal with the stopwatch - ctx.Data["IsStopwatchRunning"] = models.StopwatchExists(ctx.Doer.ID, issue.ID) + ctx.Data["IsStopwatchRunning"] = issues_model.StopwatchExists(ctx.Doer.ID, issue.ID) if !ctx.Data["IsStopwatchRunning"].(bool) { var exists bool - var sw *models.Stopwatch - if exists, sw, err = models.HasUserStopwatch(ctx, ctx.Doer.ID); err != nil { + var sw *issues_model.Stopwatch + if exists, sw, err = issues_model.HasUserStopwatch(ctx, ctx.Doer.ID); err != nil { ctx.ServerError("HasUserStopwatch", err) return } ctx.Data["HasUserStopwatch"] = exists if exists { // Add warning if the user has already a stopwatch - var otherIssue *models.Issue - if otherIssue, err = models.GetIssueByID(sw.IssueID); err != nil { + var otherIssue *issues_model.Issue + if otherIssue, err = issues_model.GetIssueByID(ctx, sw.IssueID); err != nil { ctx.ServerError("GetIssueByID", err) return } @@ -1338,7 +1338,7 @@ func ViewIssue(ctx *context.Context) { } else { ctx.Data["CanUseTimetracker"] = false } - if ctx.Data["WorkingUsers"], err = models.TotalTimes(&models.FindTrackedTimesOptions{IssueID: issue.ID}); err != nil { + if ctx.Data["WorkingUsers"], err = issues_model.TotalTimes(&issues_model.FindTrackedTimesOptions{IssueID: issue.ID}); err != nil { ctx.ServerError("TotalTimes", err) return } @@ -1366,7 +1366,7 @@ func ViewIssue(ctx *context.Context) { return } - if comment.Type == models.CommentTypeComment || comment.Type == models.CommentTypeReview { + if comment.Type == issues_model.CommentTypeComment || comment.Type == issues_model.CommentTypeReview { if err := comment.LoadAttachments(); err != nil { ctx.ServerError("LoadAttachments", err) return @@ -1396,12 +1396,12 @@ func ViewIssue(ctx *context.Context) { } marked[comment.PosterID] = comment.ShowRole participants = addParticipant(comment.Poster, participants) - } else if comment.Type == models.CommentTypeLabel { + } else if comment.Type == issues_model.CommentTypeLabel { if err = comment.LoadLabel(); err != nil { ctx.ServerError("LoadLabel", err) return } - } else if comment.Type == models.CommentTypeMilestone { + } else if comment.Type == issues_model.CommentTypeMilestone { if err = comment.LoadMilestone(); err != nil { ctx.ServerError("LoadMilestone", err) return @@ -1416,7 +1416,7 @@ func ViewIssue(ctx *context.Context) { if comment.MilestoneID > 0 && comment.Milestone == nil { comment.Milestone = ghostMilestone } - } else if comment.Type == models.CommentTypeProject { + } else if comment.Type == issues_model.CommentTypeProject { if err = comment.LoadProject(); err != nil { ctx.ServerError("LoadProject", err) @@ -1436,19 +1436,19 @@ func ViewIssue(ctx *context.Context) { comment.Project = ghostProject } - } else if comment.Type == models.CommentTypeAssignees || comment.Type == models.CommentTypeReviewRequest { + } else if comment.Type == issues_model.CommentTypeAssignees || comment.Type == issues_model.CommentTypeReviewRequest { if err = comment.LoadAssigneeUserAndTeam(); err != nil { ctx.ServerError("LoadAssigneeUserAndTeam", err) return } - } else if comment.Type == models.CommentTypeRemoveDependency || comment.Type == models.CommentTypeAddDependency { + } else if comment.Type == issues_model.CommentTypeRemoveDependency || comment.Type == issues_model.CommentTypeAddDependency { if err = comment.LoadDepIssueDetails(); err != nil { - if !models.IsErrIssueNotExist(err) { + if !issues_model.IsErrIssueNotExist(err) { ctx.ServerError("LoadDepIssueDetails", err) return } } - } else if comment.Type == models.CommentTypeCode || comment.Type == models.CommentTypeReview || comment.Type == models.CommentTypeDismissReview { + } else if comment.Type == issues_model.CommentTypeCode || comment.Type == issues_model.CommentTypeReview || comment.Type == issues_model.CommentTypeDismissReview { comment.RenderedContent, err = markdown.RenderString(&markup.RenderContext{ URLPrefix: ctx.Repo.RepoLink, Metas: ctx.Repo.Repository.ComposeMetas(), @@ -1459,7 +1459,7 @@ func ViewIssue(ctx *context.Context) { ctx.ServerError("RenderString", err) return } - if err = comment.LoadReview(); err != nil && !models.IsErrReviewNotExist(err) { + if err = comment.LoadReview(); err != nil && !issues_model.IsErrReviewNotExist(err) { ctx.ServerError("LoadReview", err) return } @@ -1502,14 +1502,14 @@ func ViewIssue(ctx *context.Context) { ctx.ServerError("LoadResolveDoer", err) return } - } else if comment.Type == models.CommentTypePullRequestPush { + } else if comment.Type == issues_model.CommentTypePullRequestPush { participants = addParticipant(comment.Poster, participants) if err = comment.LoadPushCommits(ctx); err != nil { ctx.ServerError("LoadPushCommits", err) return } - } else if comment.Type == models.CommentTypeAddTimeManual || - comment.Type == models.CommentTypeStopTracking { + } else if comment.Type == issues_model.CommentTypeAddTimeManual || + comment.Type == issues_model.CommentTypeStopTracking { // drop error since times could be pruned from DB.. _ = comment.LoadTime() } @@ -1562,7 +1562,7 @@ func ViewIssue(ctx *context.Context) { return } - if ctx.Data["CanMarkConversation"], err = models.CanMarkConversation(issue, ctx.Doer); err != nil { + if ctx.Data["CanMarkConversation"], err = issues_model.CanMarkConversation(issue, ctx.Doer); err != nil { ctx.ServerError("CanMarkConversation", err) return } @@ -1621,11 +1621,11 @@ func ViewIssue(ctx *context.Context) { if ctx.Doer != nil { showMergeInstructions = pull.ProtectedBranch.CanUserPush(ctx.Doer.ID) } - ctx.Data["IsBlockedByApprovals"] = !models.HasEnoughApprovals(ctx, pull.ProtectedBranch, pull) - ctx.Data["IsBlockedByRejection"] = models.MergeBlockedByRejectedReview(ctx, pull.ProtectedBranch, pull) - ctx.Data["IsBlockedByOfficialReviewRequests"] = models.MergeBlockedByOfficialReviewRequests(ctx, pull.ProtectedBranch, pull) - ctx.Data["IsBlockedByOutdatedBranch"] = models.MergeBlockedByOutdatedBranch(pull.ProtectedBranch, pull) - ctx.Data["GrantedApprovals"] = models.GetGrantedApprovalsCount(ctx, pull.ProtectedBranch, pull) + ctx.Data["IsBlockedByApprovals"] = !issues_model.HasEnoughApprovals(ctx, pull.ProtectedBranch, pull) + ctx.Data["IsBlockedByRejection"] = issues_model.MergeBlockedByRejectedReview(ctx, pull.ProtectedBranch, pull) + ctx.Data["IsBlockedByOfficialReviewRequests"] = issues_model.MergeBlockedByOfficialReviewRequests(ctx, pull.ProtectedBranch, pull) + ctx.Data["IsBlockedByOutdatedBranch"] = issues_model.MergeBlockedByOutdatedBranch(pull.ProtectedBranch, pull) + ctx.Data["GrantedApprovals"] = issues_model.GetGrantedApprovalsCount(ctx, pull.ProtectedBranch, pull) ctx.Data["RequireSigned"] = pull.ProtectedBranch.RequireSignedCommits ctx.Data["ChangedProtectedFiles"] = pull.ChangedProtectedFiles ctx.Data["IsBlockedByChangedProtectedFiles"] = len(pull.ChangedProtectedFiles) != 0 @@ -1655,7 +1655,7 @@ func ViewIssue(ctx *context.Context) { (!pull.HasMerged || ctx.Data["HeadBranchCommitID"] == ctx.Data["PullHeadCommitID"]) if isPullBranchDeletable && pull.HasMerged { - exist, err := models.HasUnmergedPullRequestsByHeadInfo(ctx, pull.HeadRepoID, pull.HeadBranch) + exist, err := issues_model.HasUnmergedPullRequestsByHeadInfo(ctx, pull.HeadRepoID, pull.HeadBranch) if err != nil { ctx.ServerError("HasUnmergedPullRequestsByHeadInfo", err) return @@ -1722,7 +1722,7 @@ func ViewIssue(ctx *context.Context) { } hiddenCommentTypes, _ = new(big.Int).SetString(val, 10) // we can safely ignore the failed conversion here } - ctx.Data["ShouldShowCommentType"] = func(commentType models.CommentType) bool { + ctx.Data["ShouldShowCommentType"] = func(commentType issues_model.CommentType) bool { return hiddenCommentTypes == nil || hiddenCommentTypes.Bit(int(commentType)) == 0 } @@ -1730,10 +1730,10 @@ func ViewIssue(ctx *context.Context) { } // GetActionIssue will return the issue which is used in the context. -func GetActionIssue(ctx *context.Context) *models.Issue { - issue, err := models.GetIssueByIndex(ctx.Repo.Repository.ID, ctx.ParamsInt64(":index")) +func GetActionIssue(ctx *context.Context) *issues_model.Issue { + issue, err := issues_model.GetIssueByIndex(ctx.Repo.Repository.ID, ctx.ParamsInt64(":index")) if err != nil { - ctx.NotFoundOrServerError("GetIssueByIndex", models.IsErrIssueNotExist, err) + ctx.NotFoundOrServerError("GetIssueByIndex", issues_model.IsErrIssueNotExist, err) return nil } issue.Repo = ctx.Repo.Repository @@ -1741,21 +1741,21 @@ func GetActionIssue(ctx *context.Context) *models.Issue { if ctx.Written() { return nil } - if err = issue.LoadAttributes(); err != nil { + if err = issue.LoadAttributes(ctx); err != nil { ctx.ServerError("LoadAttributes", nil) return nil } return issue } -func checkIssueRights(ctx *context.Context, issue *models.Issue) { +func checkIssueRights(ctx *context.Context, issue *issues_model.Issue) { if issue.IsPull && !ctx.Repo.CanRead(unit.TypePullRequests) || !issue.IsPull && !ctx.Repo.CanRead(unit.TypeIssues) { ctx.NotFound("IssueOrPullRequestUnitNotAllowed", nil) } } -func getActionIssues(ctx *context.Context) []*models.Issue { +func getActionIssues(ctx *context.Context) []*issues_model.Issue { commaSeparatedIssueIDs := ctx.FormString("issue_ids") if len(commaSeparatedIssueIDs) == 0 { return nil @@ -1769,7 +1769,7 @@ func getActionIssues(ctx *context.Context) []*models.Issue { } issueIDs = append(issueIDs, issueID) } - issues, err := models.GetIssuesByIDs(ctx, issueIDs) + issues, err := issues_model.GetIssuesByIDs(ctx, issueIDs) if err != nil { ctx.ServerError("GetIssuesByIDs", err) return nil @@ -1782,7 +1782,7 @@ func getActionIssues(ctx *context.Context) []*models.Issue { ctx.NotFound("IssueOrPullRequestUnitNotAllowed", nil) return nil } - if err = issue.LoadAttributes(); err != nil { + if err = issue.LoadAttributes(ctx); err != nil { ctx.ServerError("LoadAttributes", err) return nil } @@ -1792,9 +1792,9 @@ func getActionIssues(ctx *context.Context) []*models.Issue { // GetIssueInfo get an issue of a repository func GetIssueInfo(ctx *context.Context) { - issue, err := models.GetIssueWithAttrsByIndex(ctx.Repo.Repository.ID, ctx.ParamsInt64(":index")) + issue, err := issues_model.GetIssueWithAttrsByIndex(ctx.Repo.Repository.ID, ctx.ParamsInt64(":index")) if err != nil { - if models.IsErrIssueNotExist(err) { + if issues_model.IsErrIssueNotExist(err) { ctx.Error(http.StatusNotFound) } else { ctx.Error(http.StatusInternalServerError, "GetIssueByIndex", err.Error()) @@ -1916,9 +1916,9 @@ func UpdateIssueContent(ctx *context.Context) { // UpdateIssueDeadline updates an issue deadline func UpdateIssueDeadline(ctx *context.Context) { form := web.GetForm(ctx).(*api.EditDeadlineOption) - issue, err := models.GetIssueByIndex(ctx.Repo.Repository.ID, ctx.ParamsInt64(":index")) + issue, err := issues_model.GetIssueByIndex(ctx.Repo.Repository.ID, ctx.ParamsInt64(":index")) if err != nil { - if models.IsErrIssueNotExist(err) { + if issues_model.IsErrIssueNotExist(err) { ctx.NotFound("GetIssueByIndex", err) } else { ctx.Error(http.StatusInternalServerError, "GetIssueByIndex", err.Error()) @@ -1939,7 +1939,7 @@ func UpdateIssueDeadline(ctx *context.Context) { deadlineUnix = timeutil.TimeStamp(deadline.Unix()) } - if err := models.UpdateIssueDeadline(issue, deadlineUnix, ctx.Doer); err != nil { + if err := issues_model.UpdateIssueDeadline(issue, deadlineUnix, ctx.Doer); err != nil { ctx.Error(http.StatusInternalServerError, "UpdateIssueDeadline", err.Error()) return } @@ -2002,7 +2002,7 @@ func UpdateIssueAssignee(ctx *context.Context) { return } if !valid { - ctx.ServerError("canBeAssigned", models.ErrUserDoesNotHaveAccessToRepo{UserID: assigneeID, RepoName: issue.Repo.Name}) + ctx.ServerError("canBeAssigned", repo_model.ErrUserDoesNotHaveAccessToRepo{UserID: assigneeID, RepoName: issue.Repo.Name}) return } @@ -2080,7 +2080,7 @@ func UpdatePullReviewRequest(ctx *context.Context) { err = issue_service.IsValidTeamReviewRequest(ctx, team, ctx.Doer, action == "attach", issue) if err != nil { - if models.IsErrNotValidReviewRequest(err) { + if issues_model.IsErrNotValidReviewRequest(err) { log.Warn( "UpdatePullReviewRequest: refusing to add invalid team review request for UID[%d] team %s to %s#%d owned by UID[%d]: Error: %v", team.OrgID, team.Name, issue.Repo.FullName(), issue.Index, issue.Repo.ID, @@ -2118,7 +2118,7 @@ func UpdatePullReviewRequest(ctx *context.Context) { err = issue_service.IsValidReviewRequest(ctx, reviewer, ctx.Doer, action == "attach", issue, nil) if err != nil { - if models.IsErrNotValidReviewRequest(err) { + if issues_model.IsErrNotValidReviewRequest(err) { log.Warn( "UpdatePullReviewRequest: refusing to add invalid review request for %-v to %-v#%d: Error: %v", reviewer, issue.Repo, issue.Index, @@ -2215,7 +2215,7 @@ func SearchIssues(ctx *context.Context) { return } - var issues []*models.Issue + var issues []*issues_model.Issue var filteredCount int64 keyword := ctx.FormTrim("q") @@ -2264,7 +2264,7 @@ func SearchIssues(ctx *context.Context) { // Only fetch the issues if we either don't have a keyword or the search returned issues // This would otherwise return all issues if no issues were found by the search. if len(keyword) == 0 || len(issueIDs) > 0 || len(includedLabelNames) > 0 || len(includedMilestones) > 0 { - issuesOpt := &models.IssuesOptions{ + issuesOpt := &issues_model.IssuesOptions{ ListOptions: db.ListOptions{ Page: ctx.FormInt("page"), PageSize: limit, @@ -2300,7 +2300,7 @@ func SearchIssues(ctx *context.Context) { issuesOpt.ReviewRequestedID = ctxUserID } - if issues, err = models.Issues(issuesOpt); err != nil { + if issues, err = issues_model.Issues(issuesOpt); err != nil { ctx.Error(http.StatusInternalServerError, "Issues", err.Error()) return } @@ -2308,7 +2308,7 @@ func SearchIssues(ctx *context.Context) { issuesOpt.ListOptions = db.ListOptions{ Page: -1, } - if filteredCount, err = models.CountIssues(issuesOpt); err != nil { + if filteredCount, err = issues_model.CountIssues(issuesOpt); err != nil { ctx.Error(http.StatusInternalServerError, "CountIssues", err.Error()) return } @@ -2356,7 +2356,7 @@ func ListIssues(ctx *context.Context) { isClosed = util.OptionalBoolFalse } - var issues []*models.Issue + var issues []*issues_model.Issue var filteredCount int64 keyword := ctx.FormTrim("q") @@ -2374,7 +2374,7 @@ func ListIssues(ctx *context.Context) { } if splitted := strings.Split(ctx.FormString("labels"), ","); len(splitted) > 0 { - labelIDs, err = models.GetLabelIDsInRepoByNames(ctx.Repo.Repository.ID, splitted) + labelIDs, err = issues_model.GetLabelIDsInRepoByNames(ctx.Repo.Repository.ID, splitted) if err != nil { ctx.Error(http.StatusInternalServerError, err.Error()) return @@ -2443,7 +2443,7 @@ func ListIssues(ctx *context.Context) { // Only fetch the issues if we either don't have a keyword or the search returned issues // This would otherwise return all issues if no issues were found by the search. if len(keyword) == 0 || len(issueIDs) > 0 || len(labelIDs) > 0 { - issuesOpt := &models.IssuesOptions{ + issuesOpt := &issues_model.IssuesOptions{ ListOptions: listOptions, RepoID: ctx.Repo.Repository.ID, IsClosed: isClosed, @@ -2458,7 +2458,7 @@ func ListIssues(ctx *context.Context) { MentionedID: mentionedByID, } - if issues, err = models.Issues(issuesOpt); err != nil { + if issues, err = issues_model.Issues(issuesOpt); err != nil { ctx.Error(http.StatusInternalServerError, err.Error()) return } @@ -2466,7 +2466,7 @@ func ListIssues(ctx *context.Context) { issuesOpt.ListOptions = db.ListOptions{ Page: -1, } - if filteredCount, err = models.CountIssues(issuesOpt); err != nil { + if filteredCount, err = issues_model.CountIssues(issuesOpt); err != nil { ctx.Error(http.StatusInternalServerError, err.Error()) return } @@ -2493,14 +2493,14 @@ func UpdateIssueStatus(ctx *context.Context) { log.Warn("Unrecognized action: %s", action) } - if _, err := models.IssueList(issues).LoadRepositories(); err != nil { + if _, err := issues_model.IssueList(issues).LoadRepositories(); err != nil { ctx.ServerError("LoadRepositories", err) return } for _, issue := range issues { if issue.IsClosed != isClosed { if err := issue_service.ChangeStatus(issue, ctx.Doer, isClosed); err != nil { - if models.IsErrDependenciesLeft(err) { + if issues_model.IsErrDependenciesLeft(err) { ctx.JSON(http.StatusPreconditionFailed, map[string]interface{}{ "error": "cannot close this issue because it still has open dependencies", }) @@ -2564,7 +2564,7 @@ func NewComment(ctx *context.Context) { return } - var comment *models.Comment + var comment *issues_model.Comment defer func() { // Check if issue admin/poster changes the status of issue. if (ctx.Repo.CanWriteIssuesOrPulls(issue.IsPull) || (ctx.IsSigned && issue.IsPoster(ctx.Doer.ID))) && @@ -2572,14 +2572,14 @@ func NewComment(ctx *context.Context) { !(issue.IsPull && issue.PullRequest.HasMerged) { // Duplication and conflict check should apply to reopen pull request. - var pr *models.PullRequest + var pr *issues_model.PullRequest if form.Status == "reopen" && issue.IsPull { pull := issue.PullRequest var err error - pr, err = models.GetUnmergedPullRequest(pull.HeadRepoID, pull.BaseRepoID, pull.HeadBranch, pull.BaseBranch, pull.Flow) + pr, err = issues_model.GetUnmergedPullRequest(pull.HeadRepoID, pull.BaseRepoID, pull.HeadBranch, pull.BaseBranch, pull.Flow) if err != nil { - if !models.IsErrPullRequestNotExist(err) { + if !issues_model.IsErrPullRequestNotExist(err) { ctx.ServerError("GetUnmergedPullRequest", err) return } @@ -2599,7 +2599,7 @@ func NewComment(ctx *context.Context) { if err := issue_service.ChangeStatus(issue, ctx.Doer, isClosed); err != nil { log.Error("ChangeStatus: %v", err) - if models.IsErrDependenciesLeft(err) { + if issues_model.IsErrDependenciesLeft(err) { if issue.IsPull { ctx.Flash.Error(ctx.Tr("repo.issues.dependency.pr_close_blocked")) ctx.Redirect(fmt.Sprintf("%s/pulls/%d", ctx.Repo.RepoLink, issue.Index)) @@ -2648,14 +2648,14 @@ func NewComment(ctx *context.Context) { // UpdateCommentContent change comment of issue's content func UpdateCommentContent(ctx *context.Context) { - comment, err := models.GetCommentByID(ctx, ctx.ParamsInt64(":id")) + comment, err := issues_model.GetCommentByID(ctx, ctx.ParamsInt64(":id")) if err != nil { - ctx.NotFoundOrServerError("GetCommentByID", models.IsErrCommentNotExist, err) + ctx.NotFoundOrServerError("GetCommentByID", issues_model.IsErrCommentNotExist, err) return } if err := comment.LoadIssue(); err != nil { - ctx.NotFoundOrServerError("LoadIssue", models.IsErrIssueNotExist, err) + ctx.NotFoundOrServerError("LoadIssue", issues_model.IsErrIssueNotExist, err) return } @@ -2664,7 +2664,7 @@ func UpdateCommentContent(ctx *context.Context) { return } - if comment.Type != models.CommentTypeComment && comment.Type != models.CommentTypeReview && comment.Type != models.CommentTypeCode { + if comment.Type != issues_model.CommentTypeComment && comment.Type != issues_model.CommentTypeReview && comment.Type != issues_model.CommentTypeCode { ctx.Error(http.StatusNoContent) return } @@ -2714,21 +2714,21 @@ func UpdateCommentContent(ctx *context.Context) { // DeleteComment delete comment of issue func DeleteComment(ctx *context.Context) { - comment, err := models.GetCommentByID(ctx, ctx.ParamsInt64(":id")) + comment, err := issues_model.GetCommentByID(ctx, ctx.ParamsInt64(":id")) if err != nil { - ctx.NotFoundOrServerError("GetCommentByID", models.IsErrCommentNotExist, err) + ctx.NotFoundOrServerError("GetCommentByID", issues_model.IsErrCommentNotExist, err) return } if err := comment.LoadIssue(); err != nil { - ctx.NotFoundOrServerError("LoadIssue", models.IsErrIssueNotExist, err) + ctx.NotFoundOrServerError("LoadIssue", issues_model.IsErrIssueNotExist, err) return } if !ctx.IsSigned || (ctx.Doer.ID != comment.PosterID && !ctx.Repo.CanWriteIssuesOrPulls(comment.Issue.IsPull)) { ctx.Error(http.StatusForbidden) return - } else if comment.Type != models.CommentTypeComment && comment.Type != models.CommentTypeCode { + } else if comment.Type != issues_model.CommentTypeComment && comment.Type != issues_model.CommentTypeCode { ctx.Error(http.StatusNoContent) return } @@ -2790,7 +2790,7 @@ func ChangeIssueReaction(ctx *context.Context) { } // Reload new reactions issue.Reactions = nil - if err = issue.LoadAttributes(); err != nil { + if err = issue.LoadAttributes(ctx); err != nil { log.Info("issue.LoadAttributes: %s", err) break } @@ -2804,7 +2804,7 @@ func ChangeIssueReaction(ctx *context.Context) { // Reload new reactions issue.Reactions = nil - if err := issue.LoadAttributes(); err != nil { + if err := issue.LoadAttributes(ctx); err != nil { log.Info("issue.LoadAttributes: %s", err) break } @@ -2840,14 +2840,14 @@ func ChangeIssueReaction(ctx *context.Context) { // ChangeCommentReaction create a reaction for comment func ChangeCommentReaction(ctx *context.Context) { form := web.GetForm(ctx).(*forms.ReactionForm) - comment, err := models.GetCommentByID(ctx, ctx.ParamsInt64(":id")) + comment, err := issues_model.GetCommentByID(ctx, ctx.ParamsInt64(":id")) if err != nil { - ctx.NotFoundOrServerError("GetCommentByID", models.IsErrCommentNotExist, err) + ctx.NotFoundOrServerError("GetCommentByID", issues_model.IsErrCommentNotExist, err) return } if err := comment.LoadIssue(); err != nil { - ctx.NotFoundOrServerError("LoadIssue", models.IsErrIssueNotExist, err) + ctx.NotFoundOrServerError("LoadIssue", issues_model.IsErrIssueNotExist, err) return } @@ -2874,7 +2874,7 @@ func ChangeCommentReaction(ctx *context.Context) { return } - if comment.Type != models.CommentTypeComment && comment.Type != models.CommentTypeCode && comment.Type != models.CommentTypeReview { + if comment.Type != issues_model.CommentTypeComment && comment.Type != issues_model.CommentTypeCode && comment.Type != issues_model.CommentTypeReview { ctx.Error(http.StatusNoContent) return } @@ -2948,11 +2948,11 @@ func addParticipant(poster *user_model.User, participants []*user_model.User) [] return append(participants, poster) } -func filterXRefComments(ctx *context.Context, issue *models.Issue) error { +func filterXRefComments(ctx *context.Context, issue *issues_model.Issue) error { // Remove comments that the user has no permissions to see for i := 0; i < len(issue.Comments); { c := issue.Comments[i] - if models.CommentTypeIsRef(c.Type) && c.RefRepoID != issue.RepoID && c.RefRepoID != 0 { + if issues_model.CommentTypeIsRef(c.Type) && c.RefRepoID != issue.RepoID && c.RefRepoID != 0 { var err error // Set RefRepo for description in template c.RefRepo, err = repo_model.GetRepositoryByID(c.RefRepoID) @@ -2985,13 +2985,13 @@ func GetIssueAttachments(ctx *context.Context) { // GetCommentAttachments returns attachments for the comment func GetCommentAttachments(ctx *context.Context) { - comment, err := models.GetCommentByID(ctx, ctx.ParamsInt64(":id")) + comment, err := issues_model.GetCommentByID(ctx, ctx.ParamsInt64(":id")) if err != nil { - ctx.NotFoundOrServerError("GetCommentByID", models.IsErrCommentNotExist, err) + ctx.NotFoundOrServerError("GetCommentByID", issues_model.IsErrCommentNotExist, err) return } attachments := make([]*api.Attachment, 0) - if comment.Type == models.CommentTypeComment { + if comment.Type == issues_model.CommentTypeComment { if err := comment.LoadAttachments(); err != nil { ctx.ServerError("LoadAttachments", err) return @@ -3006,9 +3006,9 @@ func GetCommentAttachments(ctx *context.Context) { func updateAttachments(ctx *context.Context, item interface{}, files []string) error { var attachments []*repo_model.Attachment switch content := item.(type) { - case *models.Issue: + case *issues_model.Issue: attachments = content.Attachments - case *models.Comment: + case *issues_model.Comment: attachments = content.Attachments default: return fmt.Errorf("unknown Type: %T", content) @@ -3024,9 +3024,9 @@ func updateAttachments(ctx *context.Context, item interface{}, files []string) e var err error if len(files) > 0 { switch content := item.(type) { - case *models.Issue: - err = models.UpdateIssueAttachments(content.ID, files) - case *models.Comment: + case *issues_model.Issue: + err = issues_model.UpdateIssueAttachments(content.ID, files) + case *issues_model.Comment: err = content.UpdateAttachments(files) default: return fmt.Errorf("unknown Type: %T", content) @@ -3036,9 +3036,9 @@ func updateAttachments(ctx *context.Context, item interface{}, files []string) e } } switch content := item.(type) { - case *models.Issue: + case *issues_model.Issue: content.Attachments, err = repo_model.GetAttachmentsByIssueID(ctx, content.ID) - case *models.Comment: + case *issues_model.Comment: content.Attachments, err = repo_model.GetAttachmentsByCommentID(ctx, content.ID) default: return fmt.Errorf("unknown Type: %T", content) @@ -3060,17 +3060,17 @@ func attachmentsHTML(ctx *context.Context, attachments []*repo_model.Attachment, } // combineLabelComments combine the nearby label comments as one. -func combineLabelComments(issue *models.Issue) { - var prev, cur *models.Comment +func combineLabelComments(issue *issues_model.Issue) { + var prev, cur *issues_model.Comment for i := 0; i < len(issue.Comments); i++ { cur = issue.Comments[i] if i > 0 { prev = issue.Comments[i-1] } - if i == 0 || cur.Type != models.CommentTypeLabel || + if i == 0 || cur.Type != issues_model.CommentTypeLabel || (prev != nil && prev.PosterID != cur.PosterID) || (prev != nil && cur.CreatedUnix-prev.CreatedUnix >= 60) { - if cur.Type == models.CommentTypeLabel && cur.Label != nil { + if cur.Type == issues_model.CommentTypeLabel && cur.Label != nil { if cur.Content != "1" { cur.RemovedLabels = append(cur.RemovedLabels, cur.Label) } else { @@ -3081,7 +3081,7 @@ func combineLabelComments(issue *models.Issue) { } if cur.Label != nil { // now cur MUST be label comment - if prev.Type == models.CommentTypeLabel { // we can combine them only prev is a label comment + if prev.Type == issues_model.CommentTypeLabel { // we can combine them only prev is a label comment if cur.Content != "1" { // remove labels from the AddedLabels list if the label that was removed is already // in this list, and if it's not in this list, add the label to RemovedLabels diff --git a/routers/web/repo/issue_content_history.go b/routers/web/repo/issue_content_history.go index 407832dffe..d8a21c7fd7 100644 --- a/routers/web/repo/issue_content_history.go +++ b/routers/web/repo/issue_content_history.go @@ -11,8 +11,7 @@ import ( "net/http" "strings" - "code.gitea.io/gitea/models" - issuesModel "code.gitea.io/gitea/models/issues" + issues_model "code.gitea.io/gitea/models/issues" "code.gitea.io/gitea/models/unit" "code.gitea.io/gitea/modules/context" "code.gitea.io/gitea/modules/log" @@ -31,7 +30,7 @@ func GetContentHistoryOverview(ctx *context.Context) { } lang := ctx.Locale.Language() - editedHistoryCountMap, _ := issuesModel.QueryIssueContentHistoryEditedCountMap(ctx, issue.ID) + editedHistoryCountMap, _ := issues_model.QueryIssueContentHistoryEditedCountMap(ctx, issue.ID) ctx.JSON(http.StatusOK, map[string]interface{}{ "i18n": map[string]interface{}{ "textEdited": i18n.Tr(lang, "repo.issues.content_history.edited"), @@ -51,7 +50,7 @@ func GetContentHistoryList(ctx *context.Context) { return } - items, _ := issuesModel.FetchIssueContentHistoryList(ctx, issue.ID, commentID) + items, _ := issues_model.FetchIssueContentHistoryList(ctx, issue.ID, commentID) // render history list to HTML for frontend dropdown items: (name, value) // name is HTML of "avatar + userName + userAction + timeSince" @@ -89,8 +88,8 @@ func GetContentHistoryList(ctx *context.Context) { // canSoftDeleteContentHistory checks whether current user can soft-delete a history revision // Admins or owners can always delete history revisions. Normal users can only delete own history revisions. -func canSoftDeleteContentHistory(ctx *context.Context, issue *models.Issue, comment *models.Comment, - history *issuesModel.ContentHistory, +func canSoftDeleteContentHistory(ctx *context.Context, issue *issues_model.Issue, comment *issues_model.Comment, + history *issues_model.ContentHistory, ) bool { canSoftDelete := false if ctx.Repo.IsOwner() { @@ -118,7 +117,7 @@ func GetContentHistoryDetail(ctx *context.Context) { } historyID := ctx.FormInt64("history_id") - history, prevHistory, err := issuesModel.GetIssueContentHistoryAndPrev(ctx, historyID) + history, prevHistory, err := issues_model.GetIssueContentHistoryAndPrev(ctx, historyID) if err != nil { ctx.JSON(http.StatusNotFound, map[string]interface{}{ "message": "Can not find the content history", @@ -127,10 +126,10 @@ func GetContentHistoryDetail(ctx *context.Context) { } // get the related comment if this history revision is for a comment, otherwise the history revision is for an issue. - var comment *models.Comment + var comment *issues_model.Comment if history.CommentID != 0 { var err error - if comment, err = models.GetCommentByID(ctx, history.CommentID); err != nil { + if comment, err = issues_model.GetCommentByID(ctx, history.CommentID); err != nil { log.Error("can not get comment for issue content history %v. err=%v", historyID, err) return } @@ -186,16 +185,16 @@ func SoftDeleteContentHistory(ctx *context.Context) { commentID := ctx.FormInt64("comment_id") historyID := ctx.FormInt64("history_id") - var comment *models.Comment - var history *issuesModel.ContentHistory + var comment *issues_model.Comment + var history *issues_model.ContentHistory var err error if commentID != 0 { - if comment, err = models.GetCommentByID(ctx, commentID); err != nil { + if comment, err = issues_model.GetCommentByID(ctx, commentID); err != nil { log.Error("can not get comment for issue content history %v. err=%v", historyID, err) return } } - if history, err = issuesModel.GetIssueContentHistoryByID(ctx, historyID); err != nil { + if history, err = issues_model.GetIssueContentHistoryByID(ctx, historyID); err != nil { log.Error("can not get issue content history %v. err=%v", historyID, err) return } @@ -208,7 +207,7 @@ func SoftDeleteContentHistory(ctx *context.Context) { return } - err = issuesModel.SoftDeleteIssueContentHistory(ctx, historyID) + err = issues_model.SoftDeleteIssueContentHistory(ctx, historyID) log.Debug("soft delete issue content history. issue=%d, comment=%d, history=%d", issue.ID, commentID, historyID) ctx.JSON(http.StatusOK, map[string]interface{}{ "ok": err == nil, diff --git a/routers/web/repo/issue_dependency.go b/routers/web/repo/issue_dependency.go index ec713238c6..d8d934ea1c 100644 --- a/routers/web/repo/issue_dependency.go +++ b/routers/web/repo/issue_dependency.go @@ -7,7 +7,7 @@ package repo import ( "net/http" - "code.gitea.io/gitea/models" + issues_model "code.gitea.io/gitea/models/issues" "code.gitea.io/gitea/modules/context" "code.gitea.io/gitea/modules/setting" ) @@ -15,7 +15,7 @@ import ( // AddDependency adds new dependencies func AddDependency(ctx *context.Context) { issueIndex := ctx.ParamsInt64("index") - issue, err := models.GetIssueByIndex(ctx.Repo.Repository.ID, issueIndex) + issue, err := issues_model.GetIssueByIndex(ctx.Repo.Repository.ID, issueIndex) if err != nil { ctx.ServerError("GetIssueByIndex", err) return @@ -38,7 +38,7 @@ func AddDependency(ctx *context.Context) { defer ctx.Redirect(issue.HTMLURL()) // Dependency - dep, err := models.GetIssueByID(depID) + dep, err := issues_model.GetIssueByID(ctx, depID) if err != nil { ctx.Flash.Error(ctx.Tr("repo.issues.dependency.add_error_dep_issue_not_exist")) return @@ -56,12 +56,12 @@ func AddDependency(ctx *context.Context) { return } - err = models.CreateIssueDependency(ctx.Doer, issue, dep) + err = issues_model.CreateIssueDependency(ctx.Doer, issue, dep) if err != nil { - if models.IsErrDependencyExists(err) { + if issues_model.IsErrDependencyExists(err) { ctx.Flash.Error(ctx.Tr("repo.issues.dependency.add_error_dep_exists")) return - } else if models.IsErrCircularDependency(err) { + } else if issues_model.IsErrCircularDependency(err) { ctx.Flash.Error(ctx.Tr("repo.issues.dependency.add_error_cannot_create_circular")) return } else { @@ -74,7 +74,7 @@ func AddDependency(ctx *context.Context) { // RemoveDependency removes the dependency func RemoveDependency(ctx *context.Context) { issueIndex := ctx.ParamsInt64("index") - issue, err := models.GetIssueByIndex(ctx.Repo.Repository.ID, issueIndex) + issue, err := issues_model.GetIssueByIndex(ctx.Repo.Repository.ID, issueIndex) if err != nil { ctx.ServerError("GetIssueByIndex", err) return @@ -96,27 +96,27 @@ func RemoveDependency(ctx *context.Context) { // Dependency Type depTypeStr := ctx.Req.PostForm.Get("dependencyType") - var depType models.DependencyType + var depType issues_model.DependencyType switch depTypeStr { case "blockedBy": - depType = models.DependencyTypeBlockedBy + depType = issues_model.DependencyTypeBlockedBy case "blocking": - depType = models.DependencyTypeBlocking + depType = issues_model.DependencyTypeBlocking default: ctx.Error(http.StatusBadRequest, "GetDependecyType") return } // Dependency - dep, err := models.GetIssueByID(depID) + dep, err := issues_model.GetIssueByID(ctx, depID) if err != nil { ctx.ServerError("GetIssueByID", err) return } - if err = models.RemoveIssueDependency(ctx.Doer, issue, dep, depType); err != nil { - if models.IsErrDependencyNotExists(err) { + if err = issues_model.RemoveIssueDependency(ctx.Doer, issue, dep, depType); err != nil { + if issues_model.IsErrDependencyNotExists(err) { ctx.Flash.Error(ctx.Tr("repo.issues.dependency.add_error_dep_not_exist")) return } diff --git a/routers/web/repo/issue_label.go b/routers/web/repo/issue_label.go index 2e72d659be..7af415a8fa 100644 --- a/routers/web/repo/issue_label.go +++ b/routers/web/repo/issue_label.go @@ -7,8 +7,8 @@ package repo import ( "net/http" - "code.gitea.io/gitea/models" "code.gitea.io/gitea/models/db" + issues_model "code.gitea.io/gitea/models/issues" "code.gitea.io/gitea/models/organization" "code.gitea.io/gitea/modules/base" "code.gitea.io/gitea/modules/context" @@ -56,7 +56,7 @@ func InitializeLabels(ctx *context.Context) { // RetrieveLabels find all the labels of a repository and organization func RetrieveLabels(ctx *context.Context) { - labels, err := models.GetLabelsByRepoID(ctx, ctx.Repo.Repository.ID, ctx.FormString("sort"), db.ListOptions{}) + labels, err := issues_model.GetLabelsByRepoID(ctx, ctx.Repo.Repository.ID, ctx.FormString("sort"), db.ListOptions{}) if err != nil { ctx.ServerError("RetrieveLabels.GetLabels", err) return @@ -69,7 +69,7 @@ func RetrieveLabels(ctx *context.Context) { ctx.Data["Labels"] = labels if ctx.Repo.Owner.IsOrganization() { - orgLabels, err := models.GetLabelsByOrgID(ctx, ctx.Repo.Owner.ID, ctx.FormString("sort"), db.ListOptions{}) + orgLabels, err := issues_model.GetLabelsByOrgID(ctx, ctx.Repo.Owner.ID, ctx.FormString("sort"), db.ListOptions{}) if err != nil { ctx.ServerError("GetLabelsByOrgID", err) return @@ -111,13 +111,13 @@ func NewLabel(ctx *context.Context) { return } - l := &models.Label{ + l := &issues_model.Label{ RepoID: ctx.Repo.Repository.ID, Name: form.Title, Description: form.Description, Color: form.Color, } - if err := models.NewLabel(ctx, l); err != nil { + if err := issues_model.NewLabel(ctx, l); err != nil { ctx.ServerError("NewLabel", err) return } @@ -127,10 +127,10 @@ func NewLabel(ctx *context.Context) { // UpdateLabel update a label's name and color func UpdateLabel(ctx *context.Context) { form := web.GetForm(ctx).(*forms.CreateLabelForm) - l, err := models.GetLabelInRepoByID(ctx, ctx.Repo.Repository.ID, form.ID) + l, err := issues_model.GetLabelInRepoByID(ctx, ctx.Repo.Repository.ID, form.ID) if err != nil { switch { - case models.IsErrRepoLabelNotExist(err): + case issues_model.IsErrRepoLabelNotExist(err): ctx.Error(http.StatusNotFound) default: ctx.ServerError("UpdateLabel", err) @@ -141,7 +141,7 @@ func UpdateLabel(ctx *context.Context) { l.Name = form.Title l.Description = form.Description l.Color = form.Color - if err := models.UpdateLabel(l); err != nil { + if err := issues_model.UpdateLabel(l); err != nil { ctx.ServerError("UpdateLabel", err) return } @@ -150,7 +150,7 @@ func UpdateLabel(ctx *context.Context) { // DeleteLabel delete a label func DeleteLabel(ctx *context.Context) { - if err := models.DeleteLabel(ctx.Repo.Repository.ID, ctx.FormInt64("id")); err != nil { + if err := issues_model.DeleteLabel(ctx.Repo.Repository.ID, ctx.FormInt64("id")); err != nil { ctx.Flash.Error("DeleteLabel: " + err.Error()) } else { ctx.Flash.Success(ctx.Tr("repo.issues.label_deletion_success")) @@ -177,9 +177,9 @@ func UpdateIssueLabel(ctx *context.Context) { } } case "attach", "detach", "toggle": - label, err := models.GetLabelByID(ctx, ctx.FormInt64("id")) + label, err := issues_model.GetLabelByID(ctx, ctx.FormInt64("id")) if err != nil { - if models.IsErrRepoLabelNotExist(err) { + if issues_model.IsErrRepoLabelNotExist(err) { ctx.Error(http.StatusNotFound, "GetLabelByID") } else { ctx.ServerError("GetLabelByID", err) @@ -191,7 +191,7 @@ func UpdateIssueLabel(ctx *context.Context) { // detach if any issues already have label, otherwise attach action = "attach" for _, issue := range issues { - if models.HasIssueLabel(ctx, issue.ID, label.ID) { + if issues_model.HasIssueLabel(ctx, issue.ID, label.ID) { action = "detach" break } diff --git a/routers/web/repo/issue_label_test.go b/routers/web/repo/issue_label_test.go index 5d7a29ee93..ea078e215c 100644 --- a/routers/web/repo/issue_label_test.go +++ b/routers/web/repo/issue_label_test.go @@ -9,7 +9,7 @@ import ( "strconv" "testing" - "code.gitea.io/gitea/models" + issues_model "code.gitea.io/gitea/models/issues" "code.gitea.io/gitea/models/unittest" "code.gitea.io/gitea/modules/test" "code.gitea.io/gitea/modules/web" @@ -37,7 +37,7 @@ func TestInitializeLabels(t *testing.T) { web.SetForm(ctx, &forms.InitializeLabelsForm{TemplateName: "Default"}) InitializeLabels(ctx) assert.EqualValues(t, http.StatusSeeOther, ctx.Resp.Status()) - unittest.AssertExistsAndLoadBean(t, &models.Label{ + unittest.AssertExistsAndLoadBean(t, &issues_model.Label{ RepoID: 2, Name: "enhancement", Color: "#84b6eb", @@ -62,7 +62,7 @@ func TestRetrieveLabels(t *testing.T) { ctx.Req.Form.Set("sort", testCase.Sort) RetrieveLabels(ctx) assert.False(t, ctx.Written()) - labels, ok := ctx.Data["Labels"].([]*models.Label) + labels, ok := ctx.Data["Labels"].([]*issues_model.Label) assert.True(t, ok) if assert.Len(t, labels, len(testCase.ExpectedLabelIDs)) { for i, label := range labels { @@ -83,7 +83,7 @@ func TestNewLabel(t *testing.T) { }) NewLabel(ctx) assert.EqualValues(t, http.StatusSeeOther, ctx.Resp.Status()) - unittest.AssertExistsAndLoadBean(t, &models.Label{ + unittest.AssertExistsAndLoadBean(t, &issues_model.Label{ Name: "newlabel", Color: "#abcdef", }) @@ -102,7 +102,7 @@ func TestUpdateLabel(t *testing.T) { }) UpdateLabel(ctx) assert.EqualValues(t, http.StatusSeeOther, ctx.Resp.Status()) - unittest.AssertExistsAndLoadBean(t, &models.Label{ + unittest.AssertExistsAndLoadBean(t, &issues_model.Label{ ID: 2, Name: "newnameforlabel", Color: "#abcdef", @@ -118,8 +118,8 @@ func TestDeleteLabel(t *testing.T) { ctx.Req.Form.Set("id", "2") DeleteLabel(ctx) assert.EqualValues(t, http.StatusOK, ctx.Resp.Status()) - unittest.AssertNotExistsBean(t, &models.Label{ID: 2}) - unittest.AssertNotExistsBean(t, &models.IssueLabel{LabelID: 2}) + unittest.AssertNotExistsBean(t, &issues_model.Label{ID: 2}) + unittest.AssertNotExistsBean(t, &issues_model.IssueLabel{LabelID: 2}) assert.Equal(t, ctx.Tr("repo.issues.label_deletion_success"), ctx.Flash.SuccessMsg) } @@ -132,9 +132,9 @@ func TestUpdateIssueLabel_Clear(t *testing.T) { ctx.Req.Form.Set("action", "clear") UpdateIssueLabel(ctx) assert.EqualValues(t, http.StatusOK, ctx.Resp.Status()) - unittest.AssertNotExistsBean(t, &models.IssueLabel{IssueID: 1}) - unittest.AssertNotExistsBean(t, &models.IssueLabel{IssueID: 3}) - unittest.CheckConsistencyFor(t, &models.Label{}) + unittest.AssertNotExistsBean(t, &issues_model.IssueLabel{IssueID: 1}) + unittest.AssertNotExistsBean(t, &issues_model.IssueLabel{IssueID: 3}) + unittest.CheckConsistencyFor(t, &issues_model.Label{}) } func TestUpdateIssueLabel_Toggle(t *testing.T) { @@ -159,11 +159,11 @@ func TestUpdateIssueLabel_Toggle(t *testing.T) { UpdateIssueLabel(ctx) assert.EqualValues(t, http.StatusOK, ctx.Resp.Status()) for _, issueID := range testCase.IssueIDs { - unittest.AssertExistsIf(t, testCase.ExpectedAdd, &models.IssueLabel{ + unittest.AssertExistsIf(t, testCase.ExpectedAdd, &issues_model.IssueLabel{ IssueID: issueID, LabelID: testCase.LabelID, }) } - unittest.CheckConsistencyFor(t, &models.Label{}) + unittest.CheckConsistencyFor(t, &issues_model.Label{}) } } diff --git a/routers/web/repo/issue_lock.go b/routers/web/repo/issue_lock.go index 5ac5cac52e..a89ea47571 100644 --- a/routers/web/repo/issue_lock.go +++ b/routers/web/repo/issue_lock.go @@ -5,7 +5,7 @@ package repo import ( - "code.gitea.io/gitea/models" + issues_model "code.gitea.io/gitea/models/issues" "code.gitea.io/gitea/modules/context" "code.gitea.io/gitea/modules/web" "code.gitea.io/gitea/services/forms" @@ -32,7 +32,7 @@ func LockIssue(ctx *context.Context) { return } - if err := models.LockIssue(&models.IssueLockOptions{ + if err := issues_model.LockIssue(&issues_model.IssueLockOptions{ Doer: ctx.Doer, Issue: issue, Reason: form.Reason, @@ -57,7 +57,7 @@ func UnlockIssue(ctx *context.Context) { return } - if err := models.UnlockIssue(&models.IssueLockOptions{ + if err := issues_model.UnlockIssue(&issues_model.IssueLockOptions{ Doer: ctx.Doer, Issue: issue, }); err != nil { diff --git a/routers/web/repo/issue_stopwatch.go b/routers/web/repo/issue_stopwatch.go index 4e1f6af039..68f89b258d 100644 --- a/routers/web/repo/issue_stopwatch.go +++ b/routers/web/repo/issue_stopwatch.go @@ -8,8 +8,8 @@ import ( "net/http" "strings" - "code.gitea.io/gitea/models" "code.gitea.io/gitea/models/db" + issues_model "code.gitea.io/gitea/models/issues" "code.gitea.io/gitea/modules/context" "code.gitea.io/gitea/modules/eventsource" ) @@ -23,7 +23,7 @@ func IssueStopwatch(c *context.Context) { var showSuccessMessage bool - if !models.StopwatchExists(c.Doer.ID, issue.ID) { + if !issues_model.StopwatchExists(c.Doer.ID, issue.ID) { showSuccessMessage = true } @@ -32,7 +32,7 @@ func IssueStopwatch(c *context.Context) { return } - if err := models.CreateOrStopIssueStopwatch(c.Doer, issue); err != nil { + if err := issues_model.CreateOrStopIssueStopwatch(c.Doer, issue); err != nil { c.ServerError("CreateOrStopIssueStopwatch", err) return } @@ -56,12 +56,12 @@ func CancelStopwatch(c *context.Context) { return } - if err := models.CancelStopwatch(c.Doer, issue); err != nil { + if err := issues_model.CancelStopwatch(c.Doer, issue); err != nil { c.ServerError("CancelStopwatch", err) return } - stopwatches, err := models.GetUserStopwatches(c.Doer.ID, db.ListOptions{}) + stopwatches, err := issues_model.GetUserStopwatches(c.Doer.ID, db.ListOptions{}) if err != nil { c.ServerError("GetUserStopwatches", err) return @@ -87,7 +87,7 @@ func GetActiveStopwatch(ctx *context.Context) { return } - _, sw, err := models.HasUserStopwatch(ctx, ctx.Doer.ID) + _, sw, err := issues_model.HasUserStopwatch(ctx, ctx.Doer.ID) if err != nil { ctx.ServerError("HasUserStopwatch", err) return @@ -97,7 +97,7 @@ func GetActiveStopwatch(ctx *context.Context) { return } - issue, err := models.GetIssueByID(sw.IssueID) + issue, err := issues_model.GetIssueByID(ctx, sw.IssueID) if err != nil || issue == nil { ctx.ServerError("GetIssueByID", err) return diff --git a/routers/web/repo/issue_test.go b/routers/web/repo/issue_test.go index debd2a8a3c..ad82fe0f36 100644 --- a/routers/web/repo/issue_test.go +++ b/routers/web/repo/issue_test.go @@ -7,7 +7,7 @@ package repo import ( "testing" - "code.gitea.io/gitea/models" + issues_model "code.gitea.io/gitea/models/issues" "github.com/stretchr/testify/assert" ) @@ -15,50 +15,50 @@ import ( func TestCombineLabelComments(t *testing.T) { kases := []struct { name string - beforeCombined []*models.Comment - afterCombined []*models.Comment + beforeCombined []*issues_model.Comment + afterCombined []*issues_model.Comment }{ { name: "kase 1", - beforeCombined: []*models.Comment{ + beforeCombined: []*issues_model.Comment{ { - Type: models.CommentTypeLabel, + Type: issues_model.CommentTypeLabel, PosterID: 1, Content: "1", - Label: &models.Label{ + Label: &issues_model.Label{ Name: "kind/bug", }, CreatedUnix: 0, }, { - Type: models.CommentTypeLabel, + Type: issues_model.CommentTypeLabel, PosterID: 1, Content: "", - Label: &models.Label{ + Label: &issues_model.Label{ Name: "kind/bug", }, CreatedUnix: 0, }, { - Type: models.CommentTypeComment, + Type: issues_model.CommentTypeComment, PosterID: 1, Content: "test", CreatedUnix: 0, }, }, - afterCombined: []*models.Comment{ + afterCombined: []*issues_model.Comment{ { - Type: models.CommentTypeLabel, + Type: issues_model.CommentTypeLabel, PosterID: 1, Content: "1", CreatedUnix: 0, - AddedLabels: []*models.Label{}, - Label: &models.Label{ + AddedLabels: []*issues_model.Label{}, + Label: &issues_model.Label{ Name: "kind/bug", }, }, { - Type: models.CommentTypeComment, + Type: issues_model.CommentTypeComment, PosterID: 1, Content: "test", CreatedUnix: 0, @@ -67,63 +67,63 @@ func TestCombineLabelComments(t *testing.T) { }, { name: "kase 2", - beforeCombined: []*models.Comment{ + beforeCombined: []*issues_model.Comment{ { - Type: models.CommentTypeLabel, + Type: issues_model.CommentTypeLabel, PosterID: 1, Content: "1", - Label: &models.Label{ + Label: &issues_model.Label{ Name: "kind/bug", }, CreatedUnix: 0, }, { - Type: models.CommentTypeLabel, + Type: issues_model.CommentTypeLabel, PosterID: 1, Content: "", - Label: &models.Label{ + Label: &issues_model.Label{ Name: "kind/bug", }, CreatedUnix: 70, }, { - Type: models.CommentTypeComment, + Type: issues_model.CommentTypeComment, PosterID: 1, Content: "test", CreatedUnix: 0, }, }, - afterCombined: []*models.Comment{ + afterCombined: []*issues_model.Comment{ { - Type: models.CommentTypeLabel, + Type: issues_model.CommentTypeLabel, PosterID: 1, Content: "1", CreatedUnix: 0, - AddedLabels: []*models.Label{ + AddedLabels: []*issues_model.Label{ { Name: "kind/bug", }, }, - Label: &models.Label{ + Label: &issues_model.Label{ Name: "kind/bug", }, }, { - Type: models.CommentTypeLabel, + Type: issues_model.CommentTypeLabel, PosterID: 1, Content: "", CreatedUnix: 70, - RemovedLabels: []*models.Label{ + RemovedLabels: []*issues_model.Label{ { Name: "kind/bug", }, }, - Label: &models.Label{ + Label: &issues_model.Label{ Name: "kind/bug", }, }, { - Type: models.CommentTypeComment, + Type: issues_model.CommentTypeComment, PosterID: 1, Content: "test", CreatedUnix: 0, @@ -132,63 +132,63 @@ func TestCombineLabelComments(t *testing.T) { }, { name: "kase 3", - beforeCombined: []*models.Comment{ + beforeCombined: []*issues_model.Comment{ { - Type: models.CommentTypeLabel, + Type: issues_model.CommentTypeLabel, PosterID: 1, Content: "1", - Label: &models.Label{ + Label: &issues_model.Label{ Name: "kind/bug", }, CreatedUnix: 0, }, { - Type: models.CommentTypeLabel, + Type: issues_model.CommentTypeLabel, PosterID: 2, Content: "", - Label: &models.Label{ + Label: &issues_model.Label{ Name: "kind/bug", }, CreatedUnix: 0, }, { - Type: models.CommentTypeComment, + Type: issues_model.CommentTypeComment, PosterID: 1, Content: "test", CreatedUnix: 0, }, }, - afterCombined: []*models.Comment{ + afterCombined: []*issues_model.Comment{ { - Type: models.CommentTypeLabel, + Type: issues_model.CommentTypeLabel, PosterID: 1, Content: "1", CreatedUnix: 0, - AddedLabels: []*models.Label{ + AddedLabels: []*issues_model.Label{ { Name: "kind/bug", }, }, - Label: &models.Label{ + Label: &issues_model.Label{ Name: "kind/bug", }, }, { - Type: models.CommentTypeLabel, + Type: issues_model.CommentTypeLabel, PosterID: 2, Content: "", CreatedUnix: 0, - RemovedLabels: []*models.Label{ + RemovedLabels: []*issues_model.Label{ { Name: "kind/bug", }, }, - Label: &models.Label{ + Label: &issues_model.Label{ Name: "kind/bug", }, }, { - Type: models.CommentTypeComment, + Type: issues_model.CommentTypeComment, PosterID: 1, Content: "test", CreatedUnix: 0, @@ -197,33 +197,33 @@ func TestCombineLabelComments(t *testing.T) { }, { name: "kase 4", - beforeCombined: []*models.Comment{ + beforeCombined: []*issues_model.Comment{ { - Type: models.CommentTypeLabel, + Type: issues_model.CommentTypeLabel, PosterID: 1, Content: "1", - Label: &models.Label{ + Label: &issues_model.Label{ Name: "kind/bug", }, CreatedUnix: 0, }, { - Type: models.CommentTypeLabel, + Type: issues_model.CommentTypeLabel, PosterID: 1, Content: "1", - Label: &models.Label{ + Label: &issues_model.Label{ Name: "kind/backport", }, CreatedUnix: 10, }, }, - afterCombined: []*models.Comment{ + afterCombined: []*issues_model.Comment{ { - Type: models.CommentTypeLabel, + Type: issues_model.CommentTypeLabel, PosterID: 1, Content: "1", CreatedUnix: 10, - AddedLabels: []*models.Label{ + AddedLabels: []*issues_model.Label{ { Name: "kind/bug", }, @@ -231,7 +231,7 @@ func TestCombineLabelComments(t *testing.T) { Name: "kind/backport", }, }, - Label: &models.Label{ + Label: &issues_model.Label{ Name: "kind/bug", }, }, @@ -239,41 +239,41 @@ func TestCombineLabelComments(t *testing.T) { }, { name: "kase 5", - beforeCombined: []*models.Comment{ + beforeCombined: []*issues_model.Comment{ { - Type: models.CommentTypeLabel, + Type: issues_model.CommentTypeLabel, PosterID: 1, Content: "1", - Label: &models.Label{ + Label: &issues_model.Label{ Name: "kind/bug", }, CreatedUnix: 0, }, { - Type: models.CommentTypeComment, + Type: issues_model.CommentTypeComment, PosterID: 2, Content: "testtest", CreatedUnix: 0, }, { - Type: models.CommentTypeLabel, + Type: issues_model.CommentTypeLabel, PosterID: 1, Content: "", - Label: &models.Label{ + Label: &issues_model.Label{ Name: "kind/bug", }, CreatedUnix: 0, }, }, - afterCombined: []*models.Comment{ + afterCombined: []*issues_model.Comment{ { - Type: models.CommentTypeLabel, + Type: issues_model.CommentTypeLabel, PosterID: 1, Content: "1", - Label: &models.Label{ + Label: &issues_model.Label{ Name: "kind/bug", }, - AddedLabels: []*models.Label{ + AddedLabels: []*issues_model.Label{ { Name: "kind/bug", }, @@ -281,21 +281,21 @@ func TestCombineLabelComments(t *testing.T) { CreatedUnix: 0, }, { - Type: models.CommentTypeComment, + Type: issues_model.CommentTypeComment, PosterID: 2, Content: "testtest", CreatedUnix: 0, }, { - Type: models.CommentTypeLabel, + Type: issues_model.CommentTypeLabel, PosterID: 1, Content: "", - RemovedLabels: []*models.Label{ + RemovedLabels: []*issues_model.Label{ { Name: "kind/bug", }, }, - Label: &models.Label{ + Label: &issues_model.Label{ Name: "kind/bug", }, CreatedUnix: 0, @@ -304,53 +304,53 @@ func TestCombineLabelComments(t *testing.T) { }, { name: "kase 6", - beforeCombined: []*models.Comment{ + beforeCombined: []*issues_model.Comment{ { - Type: models.CommentTypeLabel, + Type: issues_model.CommentTypeLabel, PosterID: 1, Content: "1", - Label: &models.Label{ + Label: &issues_model.Label{ Name: "kind/bug", }, CreatedUnix: 0, }, { - Type: models.CommentTypeLabel, + Type: issues_model.CommentTypeLabel, PosterID: 1, Content: "1", - Label: &models.Label{ + Label: &issues_model.Label{ Name: "reviewed/confirmed", }, CreatedUnix: 0, }, { - Type: models.CommentTypeLabel, + Type: issues_model.CommentTypeLabel, PosterID: 1, Content: "", - Label: &models.Label{ + Label: &issues_model.Label{ Name: "kind/bug", }, CreatedUnix: 0, }, { - Type: models.CommentTypeLabel, + Type: issues_model.CommentTypeLabel, PosterID: 1, Content: "1", - Label: &models.Label{ + Label: &issues_model.Label{ Name: "kind/feature", }, CreatedUnix: 0, }, }, - afterCombined: []*models.Comment{ + afterCombined: []*issues_model.Comment{ { - Type: models.CommentTypeLabel, + Type: issues_model.CommentTypeLabel, PosterID: 1, Content: "1", - Label: &models.Label{ + Label: &issues_model.Label{ Name: "kind/bug", }, - AddedLabels: []*models.Label{ + AddedLabels: []*issues_model.Label{ { Name: "reviewed/confirmed", }, @@ -366,7 +366,7 @@ func TestCombineLabelComments(t *testing.T) { for _, kase := range kases { t.Run(kase.name, func(t *testing.T) { - issue := models.Issue{ + issue := issues_model.Issue{ Comments: kase.beforeCombined, } combineLabelComments(&issue) diff --git a/routers/web/repo/issue_timetrack.go b/routers/web/repo/issue_timetrack.go index 28274a7f7b..817a2c6d20 100644 --- a/routers/web/repo/issue_timetrack.go +++ b/routers/web/repo/issue_timetrack.go @@ -8,8 +8,8 @@ import ( "net/http" "time" - "code.gitea.io/gitea/models" "code.gitea.io/gitea/models/db" + issues_model "code.gitea.io/gitea/models/issues" "code.gitea.io/gitea/modules/context" "code.gitea.io/gitea/modules/util" "code.gitea.io/gitea/modules/web" @@ -43,7 +43,7 @@ func AddTimeManually(c *context.Context) { return } - if _, err := models.AddTime(c.Doer, issue, int64(total.Seconds()), time.Now()); err != nil { + if _, err := issues_model.AddTime(c.Doer, issue, int64(total.Seconds()), time.Now()); err != nil { c.ServerError("AddTime", err) return } @@ -62,7 +62,7 @@ func DeleteTime(c *context.Context) { return } - t, err := models.GetTrackedTimeByID(c.ParamsInt64(":timeid")) + t, err := issues_model.GetTrackedTimeByID(c.ParamsInt64(":timeid")) if err != nil { if db.IsErrNotExist(err) { c.NotFound("time not found", err) @@ -78,7 +78,7 @@ func DeleteTime(c *context.Context) { return } - if err = models.DeleteTime(t); err != nil { + if err = issues_model.DeleteTime(t); err != nil { c.ServerError("DeleteTime", err) return } diff --git a/routers/web/repo/issue_watch.go b/routers/web/repo/issue_watch.go index 53fec11cdc..5210ecf777 100644 --- a/routers/web/repo/issue_watch.go +++ b/routers/web/repo/issue_watch.go @@ -8,7 +8,7 @@ import ( "net/http" "strconv" - "code.gitea.io/gitea/models" + issues_model "code.gitea.io/gitea/models/issues" "code.gitea.io/gitea/modules/context" "code.gitea.io/gitea/modules/log" ) @@ -48,7 +48,7 @@ func IssueWatch(ctx *context.Context) { return } - if err := models.CreateOrUpdateIssueWatch(ctx.Doer.ID, issue.ID, watch); err != nil { + if err := issues_model.CreateOrUpdateIssueWatch(ctx.Doer.ID, issue.ID, watch); err != nil { ctx.ServerError("CreateOrUpdateIssueWatch", err) return } diff --git a/routers/web/repo/projects.go b/routers/web/repo/projects.go index c1805944db..51c891dbf0 100644 --- a/routers/web/repo/projects.go +++ b/routers/web/repo/projects.go @@ -10,7 +10,7 @@ import ( "net/url" "strings" - "code.gitea.io/gitea/models" + issues_model "code.gitea.io/gitea/models/issues" "code.gitea.io/gitea/models/perm" project_model "code.gitea.io/gitea/models/project" "code.gitea.io/gitea/models/unit" @@ -296,13 +296,13 @@ func ViewProject(ctx *context.Context) { boards[0].Title = ctx.Tr("repo.projects.type.uncategorized") } - issuesMap, err := models.LoadIssuesFromBoardList(boards) + issuesMap, err := issues_model.LoadIssuesFromBoardList(boards) if err != nil { ctx.ServerError("LoadIssuesOfBoards", err) return } - linkedPrsMap := make(map[int64][]*models.Issue) + linkedPrsMap := make(map[int64][]*issues_model.Issue) for _, issuesList := range issuesMap { for _, issue := range issuesList { var referencedIds []int64 @@ -313,7 +313,7 @@ func ViewProject(ctx *context.Context) { } if len(referencedIds) > 0 { - if linkedPrs, err := models.Issues(&models.IssuesOptions{ + if linkedPrs, err := issues_model.Issues(&issues_model.IssuesOptions{ IssueIDs: referencedIds, IsPull: util.OptionalBoolTrue, }); err == nil { @@ -358,7 +358,7 @@ func UpdateIssueProject(ctx *context.Context) { continue } - if err := models.ChangeProjectAssign(issue, ctx.Doer, projectID); err != nil { + if err := issues_model.ChangeProjectAssign(issue, ctx.Doer, projectID); err != nil { ctx.ServerError("ChangeProjectAssign", err) return } @@ -622,9 +622,9 @@ func MoveIssues(ctx *context.Context) { issueIDs = append(issueIDs, issue.IssueID) sortedIssueIDs[issue.Sorting] = issue.IssueID } - movedIssues, err := models.GetIssuesByIDs(ctx, issueIDs) + movedIssues, err := issues_model.GetIssuesByIDs(ctx, issueIDs) if err != nil { - if models.IsErrIssueNotExist(err) { + if issues_model.IsErrIssueNotExist(err) { ctx.NotFound("IssueNotExisting", nil) } else { ctx.ServerError("GetIssueByID", err) diff --git a/routers/web/repo/pull.go b/routers/web/repo/pull.go index c1a59ca8c0..6e8f575ad5 100644 --- a/routers/web/repo/pull.go +++ b/routers/web/repo/pull.go @@ -19,6 +19,7 @@ import ( "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" "code.gitea.io/gitea/models/organization" access_model "code.gitea.io/gitea/models/perm/access" pull_model "code.gitea.io/gitea/models/pull" @@ -256,10 +257,10 @@ func ForkPost(ctx *context.Context) { ctx.Redirect(ctxUser.HomeLink() + "/" + url.PathEscape(repo.Name)) } -func checkPullInfo(ctx *context.Context) *models.Issue { - issue, err := models.GetIssueByIndex(ctx.Repo.Repository.ID, ctx.ParamsInt64(":index")) +func checkPullInfo(ctx *context.Context) *issues_model.Issue { + issue, err := issues_model.GetIssueByIndex(ctx.Repo.Repository.ID, ctx.ParamsInt64(":index")) if err != nil { - if models.IsErrIssueNotExist(err) { + if issues_model.IsErrIssueNotExist(err) { ctx.NotFound("GetIssueByIndex", err) } else { ctx.ServerError("GetIssueByIndex", err) @@ -294,7 +295,7 @@ func checkPullInfo(ctx *context.Context) *models.Issue { if ctx.IsSigned { // Update issue-user. - if err = issue.ReadBy(ctx, ctx.Doer.ID); err != nil { + if err = models.SetIssueReadBy(ctx, issue.ID, ctx.Doer.ID); err != nil { ctx.ServerError("ReadBy", err) return nil } @@ -303,7 +304,7 @@ func checkPullInfo(ctx *context.Context) *models.Issue { return issue } -func setMergeTarget(ctx *context.Context, pull *models.PullRequest) { +func setMergeTarget(ctx *context.Context, pull *issues_model.PullRequest) { if ctx.Repo.Owner.Name == pull.MustHeadUserName() { ctx.Data["HeadTarget"] = pull.HeadBranch } else if pull.HeadRepo == nil { @@ -317,7 +318,7 @@ func setMergeTarget(ctx *context.Context, pull *models.PullRequest) { } // PrepareMergedViewPullInfo show meta information for a merged pull request view page -func PrepareMergedViewPullInfo(ctx *context.Context, issue *models.Issue) *git.CompareInfo { +func PrepareMergedViewPullInfo(ctx *context.Context, issue *issues_model.Issue) *git.CompareInfo { pull := issue.PullRequest setMergeTarget(ctx, pull) @@ -395,7 +396,7 @@ func PrepareMergedViewPullInfo(ctx *context.Context, issue *models.Issue) *git.C } // PrepareViewPullInfo show meta information for a pull request preview page -func PrepareViewPullInfo(ctx *context.Context, issue *models.Issue) *git.CompareInfo { +func PrepareViewPullInfo(ctx *context.Context, issue *issues_model.Issue) *git.CompareInfo { ctx.Data["PullRequestWorkInProgressPrefixes"] = setting.Repository.PullRequest.WorkInProgressPrefixes repo := ctx.Repo.Repository @@ -482,14 +483,14 @@ func PrepareViewPullInfo(ctx *context.Context, issue *models.Issue) *git.Compare } defer headGitRepo.Close() - if pull.Flow == models.PullRequestFlowGithub { + if pull.Flow == issues_model.PullRequestFlowGithub { headBranchExist = headGitRepo.IsBranchExist(pull.HeadBranch) } else { headBranchExist = git.IsReferenceExist(ctx, baseGitRepo.Path, pull.GetGitRefName()) } if headBranchExist { - if pull.Flow != models.PullRequestFlowGithub { + if pull.Flow != issues_model.PullRequestFlowGithub { headBranchSha, err = baseGitRepo.GetRefCommitID(pull.GetGitRefName()) } else { headBranchSha, err = headGitRepo.GetBranchCommitID(pull.HeadBranch) @@ -752,7 +753,7 @@ func ViewPullFiles(ctx *context.Context) { } if ctx.IsSigned && ctx.Doer != nil { - if ctx.Data["CanMarkConversation"], err = models.CanMarkConversation(issue, ctx.Doer); err != nil { + if ctx.Data["CanMarkConversation"], err = issues_model.CanMarkConversation(issue, ctx.Doer); err != nil { ctx.ServerError("CanMarkConversation", err) return } @@ -770,15 +771,15 @@ func ViewPullFiles(ctx *context.Context) { return } - currentReview, err := models.GetCurrentReview(ctx, ctx.Doer, issue) - if err != nil && !models.IsErrReviewNotExist(err) { + currentReview, err := issues_model.GetCurrentReview(ctx, ctx.Doer, issue) + if err != nil && !issues_model.IsErrReviewNotExist(err) { ctx.ServerError("GetCurrentReview", err) return } numPendingCodeComments := int64(0) if currentReview != nil { - numPendingCodeComments, err = models.CountComments(&models.FindCommentsOptions{ - Type: models.CommentTypeCode, + numPendingCodeComments, err = issues_model.CountComments(&issues_model.FindCommentsOptions{ + Type: issues_model.CommentTypeCode, ReviewID: currentReview.ID, IssueID: issue.ID, }) @@ -1062,7 +1063,7 @@ func MergePullRequest(ctx *context.Context) { if form.DeleteBranchAfterMerge { // Don't cleanup when other pr use this branch as head branch - exist, err := models.HasUnmergedPullRequestsByHeadInfo(ctx, pr.HeadRepoID, pr.HeadBranch) + exist, err := issues_model.HasUnmergedPullRequestsByHeadInfo(ctx, pr.HeadRepoID, pr.HeadBranch) if err != nil { ctx.ServerError("HasUnmergedPullRequestsByHeadInfo", err) return @@ -1109,9 +1110,9 @@ func CancelAutoMergePullRequest(ctx *context.Context) { ctx.Redirect(fmt.Sprintf("%s/pulls/%d", ctx.Repo.RepoLink, issue.Index)) } -func stopTimerIfAvailable(user *user_model.User, issue *models.Issue) error { - if models.StopwatchExists(user.ID, issue.ID) { - if err := models.CreateOrStopIssueStopwatch(user, issue); err != nil { +func stopTimerIfAvailable(user *user_model.User, issue *issues_model.Issue) error { + if issues_model.StopwatchExists(user.ID, issue.ID) { + if err := issues_model.CreateOrStopIssueStopwatch(user, issue); err != nil { return err } } @@ -1190,7 +1191,7 @@ func CompareAndPullRequestPost(ctx *context.Context) { return } - pullIssue := &models.Issue{ + pullIssue := &issues_model.Issue{ RepoID: repo.ID, Repo: repo, Title: form.Title, @@ -1200,7 +1201,7 @@ func CompareAndPullRequestPost(ctx *context.Context) { IsPull: true, Content: form.Content, } - pullRequest := &models.PullRequest{ + pullRequest := &issues_model.PullRequest{ HeadRepoID: ci.HeadRepo.ID, BaseRepoID: repo.ID, HeadBranch: ci.HeadBranch, @@ -1208,14 +1209,14 @@ func CompareAndPullRequestPost(ctx *context.Context) { HeadRepo: ci.HeadRepo, BaseRepo: repo, MergeBase: ci.CompareInfo.MergeBase, - Type: models.PullRequestGitea, + Type: issues_model.PullRequestGitea, AllowMaintainerEdit: form.AllowMaintainerEdit, } // FIXME: check error in the case two people send pull request at almost same time, give nice error prompt // instead of 500. if err := pull_service.NewPullRequest(ctx, repo, pullIssue, labelIDs, attachments, pullRequest, assigneeIDs); err != nil { - if models.IsErrUserDoesNotHaveAccessToRepo(err) { + if repo_model.IsErrUserDoesNotHaveAccessToRepo(err) { ctx.Error(http.StatusBadRequest, "UserDoesNotHaveAccessToRepo", err.Error()) return } else if git.IsErrPushRejected(err) { @@ -1262,7 +1263,7 @@ func CleanUpPullRequest(ctx *context.Context) { } // Don't cleanup when there are other PR's that use this branch as head branch. - exist, err := models.HasUnmergedPullRequestsByHeadInfo(ctx, pr.HeadRepoID, pr.HeadBranch) + exist, err := issues_model.HasUnmergedPullRequestsByHeadInfo(ctx, pr.HeadRepoID, pr.HeadBranch) if err != nil { ctx.ServerError("HasUnmergedPullRequestsByHeadInfo", err) return @@ -1356,7 +1357,7 @@ func CleanUpPullRequest(ctx *context.Context) { deleteBranch(ctx, pr, gitRepo) } -func deleteBranch(ctx *context.Context, pr *models.PullRequest, gitRepo *git.Repository) { +func deleteBranch(ctx *context.Context, pr *issues_model.PullRequest, gitRepo *git.Repository) { fullBranchName := pr.HeadRepo.Owner.Name + "/" + pr.HeadBranch if err := repo_service.DeleteBranch(ctx.Doer, pr.HeadRepo, gitRepo, pr.HeadBranch); err != nil { switch { @@ -1373,7 +1374,7 @@ func deleteBranch(ctx *context.Context, pr *models.PullRequest, gitRepo *git.Rep return } - if err := models.AddDeletePRBranchComment(ctx, ctx.Doer, pr.BaseRepo, pr.IssueID, pr.HeadBranch); err != nil { + if err := issues_model.AddDeletePRBranchComment(ctx, ctx.Doer, pr.BaseRepo, pr.IssueID, pr.HeadBranch); err != nil { // Do not fail here as branch has already been deleted log.Error("DeleteBranch: %v", err) } @@ -1393,9 +1394,9 @@ func DownloadPullPatch(ctx *context.Context) { // DownloadPullDiffOrPatch render a pull's raw diff or patch func DownloadPullDiffOrPatch(ctx *context.Context, patch bool) { - pr, err := models.GetPullRequestByIndex(ctx, ctx.Repo.Repository.ID, ctx.ParamsInt64(":index")) + pr, err := issues_model.GetPullRequestByIndex(ctx, ctx.Repo.Repository.ID, ctx.ParamsInt64(":index")) if err != nil { - if models.IsErrPullRequestNotExist(err) { + if issues_model.IsErrPullRequestNotExist(err) { ctx.NotFound("GetPullRequestByIndex", err) } else { ctx.ServerError("GetPullRequestByIndex", err) @@ -1435,8 +1436,8 @@ func UpdatePullRequestTarget(ctx *context.Context) { } if err := pull_service.ChangeTargetBranch(ctx, pr, ctx.Doer, targetBranch); err != nil { - if models.IsErrPullRequestAlreadyExists(err) { - err := err.(models.ErrPullRequestAlreadyExists) + if issues_model.IsErrPullRequestAlreadyExists(err) { + err := err.(issues_model.ErrPullRequestAlreadyExists) RepoRelPath := ctx.Repo.Owner.Name + "/" + ctx.Repo.Repository.Name errorMessage := ctx.Tr("repo.pulls.has_pull_request", html.EscapeString(ctx.Repo.RepoLink+"/pulls/"+strconv.FormatInt(err.IssueID, 10)), html.EscapeString(RepoRelPath), err.IssueID) // FIXME: Creates url insidde locale string @@ -1446,7 +1447,7 @@ func UpdatePullRequestTarget(ctx *context.Context) { "error": err.Error(), "user_error": errorMessage, }) - } else if models.IsErrIssueIsClosed(err) { + } else if issues_model.IsErrIssueIsClosed(err) { errorMessage := ctx.Tr("repo.pulls.is_closed") ctx.Flash.Error(errorMessage) @@ -1486,9 +1487,9 @@ func UpdatePullRequestTarget(ctx *context.Context) { func SetAllowEdits(ctx *context.Context) { form := web.GetForm(ctx).(*forms.UpdateAllowEditsForm) - pr, err := models.GetPullRequestByIndex(ctx, ctx.Repo.Repository.ID, ctx.ParamsInt64(":index")) + pr, err := issues_model.GetPullRequestByIndex(ctx, ctx.Repo.Repository.ID, ctx.ParamsInt64(":index")) if err != nil { - if models.IsErrPullRequestNotExist(err) { + if issues_model.IsErrPullRequestNotExist(err) { ctx.NotFound("GetPullRequestByIndex", err) } else { ctx.ServerError("GetPullRequestByIndex", err) diff --git a/routers/web/repo/pull_review.go b/routers/web/repo/pull_review.go index e051290200..cc7ae9bbfa 100644 --- a/routers/web/repo/pull_review.go +++ b/routers/web/repo/pull_review.go @@ -8,7 +8,7 @@ import ( "fmt" "net/http" - "code.gitea.io/gitea/models" + issues_model "code.gitea.io/gitea/models/issues" pull_model "code.gitea.io/gitea/models/pull" "code.gitea.io/gitea/modules/base" "code.gitea.io/gitea/modules/context" @@ -31,8 +31,8 @@ func RenderNewCodeCommentForm(ctx *context.Context) { if !issue.IsPull { return } - currentReview, err := models.GetCurrentReview(ctx, ctx.Doer, issue) - if err != nil && !models.IsErrReviewNotExist(err) { + currentReview, err := issues_model.GetCurrentReview(ctx, ctx.Doer, issue) + if err != nil && !issues_model.IsErrReviewNotExist(err) { ctx.ServerError("GetCurrentReview", err) return } @@ -107,7 +107,7 @@ func UpdateResolveConversation(ctx *context.Context) { action := ctx.FormString("action") commentID := ctx.FormInt64("comment_id") - comment, err := models.GetCommentByID(ctx, commentID) + comment, err := issues_model.GetCommentByID(ctx, commentID) if err != nil { ctx.ServerError("GetIssueByID", err) return @@ -119,7 +119,7 @@ func UpdateResolveConversation(ctx *context.Context) { } var permResult bool - if permResult, err = models.CanMarkConversation(comment.Issue, ctx.Doer); err != nil { + if permResult, err = issues_model.CanMarkConversation(comment.Issue, ctx.Doer); err != nil { ctx.ServerError("CanMarkConversation", err) return } @@ -134,7 +134,7 @@ func UpdateResolveConversation(ctx *context.Context) { } if action == "Resolve" || action == "UnResolve" { - err = models.MarkConversation(comment, ctx.Doer, action == "Resolve") + err = issues_model.MarkConversation(comment, ctx.Doer, action == "Resolve") if err != nil { ctx.ServerError("MarkConversation", err) return @@ -153,8 +153,8 @@ func UpdateResolveConversation(ctx *context.Context) { }) } -func renderConversation(ctx *context.Context, comment *models.Comment) { - comments, err := models.FetchCodeCommentsByLine(ctx, comment.Issue, ctx.Doer, comment.TreePath, comment.Line) +func renderConversation(ctx *context.Context, comment *issues_model.Comment) { + comments, err := issues_model.FetchCodeCommentsByLine(ctx, comment.Issue, ctx.Doer, comment.TreePath, comment.Line) if err != nil { ctx.ServerError("FetchCodeCommentsByLine", err) return @@ -194,15 +194,15 @@ func SubmitReview(ctx *context.Context) { reviewType := form.ReviewType() switch reviewType { - case models.ReviewTypeUnknown: + case issues_model.ReviewTypeUnknown: ctx.ServerError("ReviewType", fmt.Errorf("unknown ReviewType: %s", form.Type)) return // can not approve/reject your own PR - case models.ReviewTypeApprove, models.ReviewTypeReject: + case issues_model.ReviewTypeApprove, issues_model.ReviewTypeReject: if issue.IsPoster(ctx.Doer.ID) { var translated string - if reviewType == models.ReviewTypeApprove { + if reviewType == issues_model.ReviewTypeApprove { translated = ctx.Tr("repo.issues.review.self.approval") } else { translated = ctx.Tr("repo.issues.review.self.rejection") @@ -221,7 +221,7 @@ func SubmitReview(ctx *context.Context) { _, comm, err := pull_service.SubmitReview(ctx, ctx.Doer, ctx.Repo.GitRepo, issue, reviewType, form.Content, form.CommitID, attachments) if err != nil { - if models.IsContentEmptyErr(err) { + if issues_model.IsContentEmptyErr(err) { ctx.Flash.Error(ctx.Tr("repo.issues.review.content.empty")) ctx.Redirect(fmt.Sprintf("%s/pulls/%d/files", ctx.Repo.RepoLink, issue.Index)) } else { diff --git a/routers/web/user/home.go b/routers/web/user/home.go index 9b4fc652f1..7fe80a2a4b 100644 --- a/routers/web/user/home.go +++ b/routers/web/user/home.go @@ -385,17 +385,17 @@ func buildIssueOverview(ctx *context.Context, unitType unit.Type) { viewType = ctx.FormString("type") switch viewType { case "assigned": - filterMode = models.FilterModeAssign + filterMode = issues_model.FilterModeAssign case "created_by": - filterMode = models.FilterModeCreate + filterMode = issues_model.FilterModeCreate case "mentioned": - filterMode = models.FilterModeMention + filterMode = issues_model.FilterModeMention case "review_requested": - filterMode = models.FilterModeReviewRequested + filterMode = issues_model.FilterModeReviewRequested case "your_repositories": fallthrough default: - filterMode = models.FilterModeYourRepositories + filterMode = issues_model.FilterModeYourRepositories viewType = "your_repositories" } @@ -416,7 +416,7 @@ func buildIssueOverview(ctx *context.Context, unitType unit.Type) { } isPullList := unitType == unit.TypePullRequests - opts := &models.IssuesOptions{ + opts := &issues_model.IssuesOptions{ IsPull: util.OptionalBoolOf(isPullList), SortType: sortType, IsArchived: util.OptionalBoolFalse, @@ -450,15 +450,15 @@ func buildIssueOverview(ctx *context.Context, unitType unit.Type) { } switch filterMode { - case models.FilterModeAll: - case models.FilterModeYourRepositories: - case models.FilterModeAssign: + case issues_model.FilterModeAll: + case issues_model.FilterModeYourRepositories: + case issues_model.FilterModeAssign: opts.AssigneeID = ctx.Doer.ID - case models.FilterModeCreate: + case issues_model.FilterModeCreate: opts.PosterID = ctx.Doer.ID - case models.FilterModeMention: + case issues_model.FilterModeMention: opts.MentionedID = ctx.Doer.ID - case models.FilterModeReviewRequested: + case issues_model.FilterModeReviewRequested: opts.ReviewRequestedID = ctx.Doer.ID } @@ -491,7 +491,7 @@ func buildIssueOverview(ctx *context.Context, unitType unit.Type) { // USING NON-FINAL STATE OF opts FOR A QUERY. var issueCountByRepo map[int64]int64 if !forceEmpty { - issueCountByRepo, err = models.CountIssuesByRepo(opts) + issueCountByRepo, err = issues_model.CountIssuesByRepo(opts) if err != nil { ctx.ServerError("CountIssuesByRepo", err) return @@ -532,15 +532,15 @@ func buildIssueOverview(ctx *context.Context, unitType unit.Type) { // Slice of Issues that will be displayed on the overview page // USING FINAL STATE OF opts FOR A QUERY. - var issues []*models.Issue + var issues []*issues_model.Issue if !forceEmpty { - issues, err = models.Issues(opts) + issues, err = issues_model.Issues(opts) if err != nil { ctx.ServerError("Issues", err) return } } else { - issues = []*models.Issue{} + issues = []*issues_model.Issue{} } // ---------------------------------- @@ -578,9 +578,9 @@ func buildIssueOverview(ctx *context.Context, unitType unit.Type) { // ------------------------------- // Fill stats to post to ctx.Data. // ------------------------------- - var issueStats *models.IssueStats + var issueStats *issues_model.IssueStats if !forceEmpty { - statsOpts := models.UserIssueStatsOptions{ + statsOpts := issues_model.UserIssueStatsOptions{ UserID: ctx.Doer.ID, FilterMode: filterMode, IsPull: isPullList, @@ -592,13 +592,13 @@ func buildIssueOverview(ctx *context.Context, unitType unit.Type) { Team: team, } - issueStats, err = models.GetUserIssueStats(statsOpts) + issueStats, err = issues_model.GetUserIssueStats(statsOpts) if err != nil { ctx.ServerError("GetUserIssueStats Shown", err) return } } else { - issueStats = &models.IssueStats{} + issueStats = &issues_model.IssueStats{} } // Will be posted to ctx.Data. @@ -623,7 +623,7 @@ func buildIssueOverview(ctx *context.Context, unitType unit.Type) { ctx.Data["Issues"] = issues - approvalCounts, err := models.IssueList(issues).GetApprovalCounts(ctx) + approvalCounts, err := issues_model.IssueList(issues).GetApprovalCounts(ctx) if err != nil { ctx.ServerError("ApprovalCounts", err) return @@ -633,11 +633,11 @@ func buildIssueOverview(ctx *context.Context, unitType unit.Type) { if !ok || len(counts) == 0 { return 0 } - reviewTyp := models.ReviewTypeApprove + reviewTyp := issues_model.ReviewTypeApprove if typ == "reject" { - reviewTyp = models.ReviewTypeReject + reviewTyp = issues_model.ReviewTypeReject } else if typ == "waiting" { - reviewTyp = models.ReviewTypeRequest + reviewTyp = issues_model.ReviewTypeRequest } for _, count := range counts { if count.Type == reviewTyp { @@ -708,12 +708,12 @@ func getRepoIDs(reposQuery string) []int64 { return repoIDs } -func issueIDsFromSearch(ctx *context.Context, ctxUser *user_model.User, keyword string, opts *models.IssuesOptions) ([]int64, error) { +func issueIDsFromSearch(ctx *context.Context, ctxUser *user_model.User, keyword string, opts *issues_model.IssuesOptions) ([]int64, error) { if len(keyword) == 0 { return []int64{}, nil } - searchRepoIDs, err := models.GetRepoIDsForIssuesOptions(opts, ctxUser) + searchRepoIDs, err := issues_model.GetRepoIDsForIssuesOptions(opts, ctxUser) if err != nil { return nil, fmt.Errorf("GetRepoIDsForIssuesOptions: %v", err) } diff --git a/routers/web/user/stop_watch.go b/routers/web/user/stop_watch.go index 4b16c9aeda..f40d850fc1 100644 --- a/routers/web/user/stop_watch.go +++ b/routers/web/user/stop_watch.go @@ -7,15 +7,15 @@ package user import ( "net/http" - "code.gitea.io/gitea/models" "code.gitea.io/gitea/models/db" + issues_model "code.gitea.io/gitea/models/issues" "code.gitea.io/gitea/modules/context" "code.gitea.io/gitea/modules/convert" ) // GetStopwatches get all stopwatches func GetStopwatches(ctx *context.Context) { - sws, err := models.GetUserStopwatches(ctx.Doer.ID, db.ListOptions{ + sws, err := issues_model.GetUserStopwatches(ctx.Doer.ID, db.ListOptions{ Page: ctx.FormInt("page"), PageSize: convert.ToCorrectPageSize(ctx.FormInt("limit")), }) @@ -24,7 +24,7 @@ func GetStopwatches(ctx *context.Context) { return } - count, err := models.CountUserStopwatches(ctx.Doer.ID) + count, err := issues_model.CountUserStopwatches(ctx.Doer.ID) if err != nil { ctx.Error(http.StatusInternalServerError, err.Error()) return diff --git a/services/agit/agit.go b/services/agit/agit.go index cc520dbc76..7666093c51 100644 --- a/services/agit/agit.go +++ b/services/agit/agit.go @@ -10,7 +10,8 @@ import ( "os" "strings" - "code.gitea.io/gitea/models" + 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/context" "code.gitea.io/gitea/modules/git" @@ -97,9 +98,9 @@ func ProcReceive(ctx *context.PrivateContext, opts *private.HookOptions) []priva headBranch = curentTopicBranch } - pr, err := models.GetUnmergedPullRequest(repo.ID, repo.ID, headBranch, baseBranchName, models.PullRequestFlowAGit) + pr, err := issues_model.GetUnmergedPullRequest(repo.ID, repo.ID, headBranch, baseBranchName, issues_model.PullRequestFlowAGit) if err != nil { - if !models.IsErrPullRequestNotExist(err) { + if !issues_model.IsErrPullRequestNotExist(err) { log.Error("Failed to get unmerged agit flow pull request in repository: %s/%s Error: %v", ownerName, repoName, err) ctx.JSON(http.StatusInternalServerError, map[string]interface{}{ "Err": fmt.Sprintf("Failed to get unmerged agit flow pull request in repository: %s/%s Error: %v", ownerName, repoName, err), @@ -134,7 +135,7 @@ func ProcReceive(ctx *context.PrivateContext, opts *private.HookOptions) []priva return nil } - prIssue := &models.Issue{ + prIssue := &issues_model.Issue{ RepoID: repo.ID, Title: title, PosterID: pusher.ID, @@ -143,7 +144,7 @@ func ProcReceive(ctx *context.PrivateContext, opts *private.HookOptions) []priva Content: description, } - pr := &models.PullRequest{ + pr := &issues_model.PullRequest{ HeadRepoID: repo.ID, BaseRepoID: repo.ID, HeadBranch: headBranch, @@ -152,12 +153,12 @@ func ProcReceive(ctx *context.PrivateContext, opts *private.HookOptions) []priva HeadRepo: repo, BaseRepo: repo, MergeBase: "", - Type: models.PullRequestGitea, - Flow: models.PullRequestFlowAGit, + Type: issues_model.PullRequestGitea, + Flow: issues_model.PullRequestFlowAGit, } if err := pull_service.NewPullRequest(ctx, repo, prIssue, []int64{}, []string{}, pr, []int64{}); err != nil { - if models.IsErrUserDoesNotHaveAccessToRepo(err) { + if repo_model.IsErrUserDoesNotHaveAccessToRepo(err) { ctx.Error(http.StatusBadRequest, "UserDoesNotHaveAccessToRepo", err.Error()) return nil } @@ -249,7 +250,7 @@ func ProcReceive(ctx *context.PrivateContext, opts *private.HookOptions) []priva }) return nil } - comment, err := models.CreatePushPullComment(ctx, pusher, pr, oldCommitID, opts.NewCommitIDs[i]) + comment, err := issues_model.CreatePushPullComment(ctx, pusher, pr, oldCommitID, opts.NewCommitIDs[i]) if err == nil && comment != nil { notification.NotifyPullRequestPushCommits(pusher, pr, comment) } @@ -270,7 +271,7 @@ func ProcReceive(ctx *context.PrivateContext, opts *private.HookOptions) []priva // UserNameChanged handle user name change for agit flow pull func UserNameChanged(user *user_model.User, newName string) error { - pulls, err := models.GetAllUnmergedAgitPullRequestByPoster(user.ID) + pulls, err := issues_model.GetAllUnmergedAgitPullRequestByPoster(user.ID) if err != nil { return err } diff --git a/services/asymkey/sign.go b/services/asymkey/sign.go index 0f74cd4b2a..edfd0f6cad 100644 --- a/services/asymkey/sign.go +++ b/services/asymkey/sign.go @@ -9,11 +9,11 @@ import ( "fmt" "strings" - "code.gitea.io/gitea/models" asymkey_model "code.gitea.io/gitea/models/asymkey" "code.gitea.io/gitea/models/auth" "code.gitea.io/gitea/models/db" git_model "code.gitea.io/gitea/models/git" + issues_model "code.gitea.io/gitea/models/issues" user_model "code.gitea.io/gitea/models/user" "code.gitea.io/gitea/modules/git" "code.gitea.io/gitea/modules/log" @@ -271,7 +271,7 @@ Loop: } // SignMerge determines if we should sign a PR merge commit to the base repository -func SignMerge(ctx context.Context, pr *models.PullRequest, u *user_model.User, tmpBasePath, baseCommit, headCommit string) (bool, string, *git.Signature, error) { +func SignMerge(ctx context.Context, pr *issues_model.PullRequest, u *user_model.User, tmpBasePath, baseCommit, headCommit string) (bool, string, *git.Signature, error) { if err := pr.LoadBaseRepoCtx(ctx); err != nil { log.Error("Unable to get Base Repo for pull request") return false, "", nil, err @@ -318,7 +318,7 @@ Loop: if protectedBranch == nil { return false, "", nil, &ErrWontSign{approved} } - if models.GetGrantedApprovalsCount(ctx, protectedBranch, pr) < 1 { + if issues_model.GetGrantedApprovalsCount(ctx, protectedBranch, pr) < 1 { return false, "", nil, &ErrWontSign{approved} } case baseSigned: diff --git a/services/automerge/automerge.go b/services/automerge/automerge.go index 3c7346ab58..d0f83f4a93 100644 --- a/services/automerge/automerge.go +++ b/services/automerge/automerge.go @@ -11,8 +11,8 @@ import ( "strconv" "strings" - "code.gitea.io/gitea/models" "code.gitea.io/gitea/models/db" + issues_model "code.gitea.io/gitea/models/issues" access_model "code.gitea.io/gitea/models/perm/access" pull_model "code.gitea.io/gitea/models/pull" repo_model "code.gitea.io/gitea/models/repo" @@ -52,7 +52,7 @@ func handle(data ...queue.Data) []queue.Data { return nil } -func addToQueue(pr *models.PullRequest, sha string) { +func addToQueue(pr *issues_model.PullRequest, sha string) { if err := prAutoMergeQueue.PushFunc(fmt.Sprintf("%d_%s", pr.ID, sha), func() error { log.Trace("Adding pullID: %d to the pull requests patch checking queue with sha %s", pr.ID, sha) return nil @@ -62,7 +62,7 @@ func addToQueue(pr *models.PullRequest, sha string) { } // ScheduleAutoMerge if schedule is false and no error, pull can be merged directly -func ScheduleAutoMerge(ctx context.Context, doer *user_model.User, pull *models.PullRequest, style repo_model.MergeStyle, message string) (scheduled bool, err error) { +func ScheduleAutoMerge(ctx context.Context, doer *user_model.User, pull *issues_model.PullRequest, style repo_model.MergeStyle, message string) (scheduled bool, err error) { err = db.WithTx(func(ctx context.Context) error { lastCommitStatus, err := pull_service.GetPullRequestCommitStatusState(ctx, pull) if err != nil { @@ -79,27 +79,27 @@ func ScheduleAutoMerge(ctx context.Context, doer *user_model.User, pull *models. } scheduled = true - _, err = models.CreateAutoMergeComment(ctx, models.CommentTypePRScheduledToAutoMerge, pull, doer) + _, err = issues_model.CreateAutoMergeComment(ctx, issues_model.CommentTypePRScheduledToAutoMerge, pull, doer) return err }, ctx) return } // RemoveScheduledAutoMerge cancels a previously scheduled pull request -func RemoveScheduledAutoMerge(ctx context.Context, doer *user_model.User, pull *models.PullRequest) error { +func RemoveScheduledAutoMerge(ctx context.Context, doer *user_model.User, pull *issues_model.PullRequest) error { return db.WithTx(func(ctx context.Context) error { if err := pull_model.DeleteScheduledAutoMerge(ctx, pull.ID); err != nil { return err } - _, err := models.CreateAutoMergeComment(ctx, models.CommentTypePRUnScheduledToAutoMerge, pull, doer) + _, err := issues_model.CreateAutoMergeComment(ctx, issues_model.CommentTypePRUnScheduledToAutoMerge, pull, doer) return err }, ctx) } // MergeScheduledPullRequest merges a previously scheduled pull request when all checks succeeded func MergeScheduledPullRequest(ctx context.Context, sha string, repo *repo_model.Repository) error { - pulls, err := getPullRequestsByHeadSHA(ctx, sha, repo, func(pr *models.PullRequest) bool { + pulls, err := getPullRequestsByHeadSHA(ctx, sha, repo, func(pr *issues_model.PullRequest) bool { return !pr.HasMerged && pr.CanAutoMerge() }) if err != nil { @@ -113,7 +113,7 @@ func MergeScheduledPullRequest(ctx context.Context, sha string, repo *repo_model return nil } -func getPullRequestsByHeadSHA(ctx context.Context, sha string, repo *repo_model.Repository, filter func(*models.PullRequest) bool) (map[int64]*models.PullRequest, error) { +func getPullRequestsByHeadSHA(ctx context.Context, sha string, repo *repo_model.Repository, filter func(*issues_model.PullRequest) bool) (map[int64]*issues_model.PullRequest, error) { gitRepo, err := git.OpenRepository(ctx, repo.RepoPath()) if err != nil { return nil, err @@ -125,7 +125,7 @@ func getPullRequestsByHeadSHA(ctx context.Context, sha string, repo *repo_model. return nil, err } - pulls := make(map[int64]*models.PullRequest) + pulls := make(map[int64]*issues_model.PullRequest) for _, ref := range refs { // Each pull branch starts with refs/pull/ we then go from there to find the index of the pr and then @@ -145,10 +145,10 @@ func getPullRequestsByHeadSHA(ctx context.Context, sha string, repo *repo_model. continue } - p, err := models.GetPullRequestByIndex(ctx, repo.ID, prIndex) + p, err := issues_model.GetPullRequestByIndex(ctx, repo.ID, prIndex) if err != nil { // If there is no pull request for this branch, we don't try to merge it. - if models.IsErrPullRequestNotExist(err) { + if issues_model.IsErrPullRequestNotExist(err) { continue } return nil, err @@ -168,7 +168,7 @@ func handlePull(pullID int64, sha string) { fmt.Sprintf("Handle AutoMerge of pull[%d] with sha[%s]", pullID, sha)) defer finished() - pr, err := models.GetPullRequestByID(ctx, pullID) + pr, err := issues_model.GetPullRequestByID(ctx, pullID) if err != nil { log.Error("GetPullRequestByID[%d]: %v", pullID, err) return diff --git a/services/comments/comments.go b/services/comments/comments.go index b80fddf93f..c40631359b 100644 --- a/services/comments/comments.go +++ b/services/comments/comments.go @@ -5,9 +5,8 @@ package comments import ( - "code.gitea.io/gitea/models" "code.gitea.io/gitea/models/db" - "code.gitea.io/gitea/models/issues" + 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/notification" @@ -15,9 +14,9 @@ import ( ) // CreateIssueComment creates a plain issue comment. -func CreateIssueComment(doer *user_model.User, repo *repo_model.Repository, issue *models.Issue, content string, attachments []string) (*models.Comment, error) { - comment, err := models.CreateComment(&models.CreateCommentOptions{ - Type: models.CommentTypeComment, +func CreateIssueComment(doer *user_model.User, repo *repo_model.Repository, issue *issues_model.Issue, content string, attachments []string) (*issues_model.Comment, error) { + comment, err := issues_model.CreateComment(&issues_model.CreateCommentOptions{ + Type: issues_model.CommentTypeComment, Doer: doer, Repo: repo, Issue: issue, @@ -28,7 +27,7 @@ func CreateIssueComment(doer *user_model.User, repo *repo_model.Repository, issu return nil, err } - mentions, err := models.FindAndUpdateIssueMentions(db.DefaultContext, issue, doer, comment.Content) + mentions, err := issues_model.FindAndUpdateIssueMentions(db.DefaultContext, issue, doer, comment.Content) if err != nil { return nil, err } @@ -39,28 +38,28 @@ func CreateIssueComment(doer *user_model.User, repo *repo_model.Repository, issu } // UpdateComment updates information of comment. -func UpdateComment(c *models.Comment, doer *user_model.User, oldContent string) error { +func UpdateComment(c *issues_model.Comment, doer *user_model.User, oldContent string) error { needsContentHistory := c.Content != oldContent && - (c.Type == models.CommentTypeComment || c.Type == models.CommentTypeReview || c.Type == models.CommentTypeCode) + (c.Type == issues_model.CommentTypeComment || c.Type == issues_model.CommentTypeReview || c.Type == issues_model.CommentTypeCode) if needsContentHistory { - hasContentHistory, err := issues.HasIssueContentHistory(db.DefaultContext, c.IssueID, c.ID) + hasContentHistory, err := issues_model.HasIssueContentHistory(db.DefaultContext, c.IssueID, c.ID) if err != nil { return err } if !hasContentHistory { - if err = issues.SaveIssueContentHistory(db.DefaultContext, c.PosterID, c.IssueID, c.ID, + if err = issues_model.SaveIssueContentHistory(db.DefaultContext, c.PosterID, c.IssueID, c.ID, c.CreatedUnix, oldContent, true); err != nil { return err } } } - if err := models.UpdateComment(c, doer); err != nil { + if err := issues_model.UpdateComment(c, doer); err != nil { return err } if needsContentHistory { - err := issues.SaveIssueContentHistory(db.DefaultContext, doer.ID, c.IssueID, c.ID, timeutil.TimeStampNow(), c.Content, false) + err := issues_model.SaveIssueContentHistory(db.DefaultContext, doer.ID, c.IssueID, c.ID, timeutil.TimeStampNow(), c.Content, false) if err != nil { return err } @@ -72,8 +71,17 @@ func UpdateComment(c *models.Comment, doer *user_model.User, oldContent string) } // DeleteComment deletes the comment -func DeleteComment(doer *user_model.User, comment *models.Comment) error { - if err := models.DeleteComment(comment); err != nil { +func DeleteComment(doer *user_model.User, comment *issues_model.Comment) error { + ctx, committer, err := db.TxContext() + if err != nil { + return err + } + defer committer.Close() + + if err := issues_model.DeleteComment(ctx, comment); err != nil { + return err + } + if err := committer.Commit(); err != nil { return err } diff --git a/services/forms/repo_form.go b/services/forms/repo_form.go index 23ac1abe3c..c9327bbd9b 100644 --- a/services/forms/repo_form.go +++ b/services/forms/repo_form.go @@ -11,6 +11,7 @@ import ( "strings" "code.gitea.io/gitea/models" + issues_model "code.gitea.io/gitea/models/issues" project_model "code.gitea.io/gitea/models/project" "code.gitea.io/gitea/modules/context" "code.gitea.io/gitea/modules/setting" @@ -636,18 +637,18 @@ func (f *SubmitReviewForm) Validate(req *http.Request, errs binding.Errors) bind } // ReviewType will return the corresponding ReviewType for type -func (f SubmitReviewForm) ReviewType() models.ReviewType { +func (f SubmitReviewForm) ReviewType() issues_model.ReviewType { switch f.Type { case "approve": - return models.ReviewTypeApprove + return issues_model.ReviewTypeApprove case "comment": - return models.ReviewTypeComment + return issues_model.ReviewTypeComment case "reject": - return models.ReviewTypeReject + return issues_model.ReviewTypeReject case "": - return models.ReviewTypeComment // default to comment when doing quick-submit (Ctrl+Enter) on the review form + return issues_model.ReviewTypeComment // default to comment when doing quick-submit (Ctrl+Enter) on the review form default: - return models.ReviewTypeUnknown + return issues_model.ReviewTypeUnknown } } @@ -655,7 +656,7 @@ func (f SubmitReviewForm) ReviewType() models.ReviewType { func (f SubmitReviewForm) HasEmptyContent() bool { reviewType := f.ReviewType() - return (reviewType == models.ReviewTypeComment || reviewType == models.ReviewTypeReject) && + return (reviewType == issues_model.ReviewTypeComment || reviewType == issues_model.ReviewTypeReject) && len(strings.TrimSpace(f.Content)) == 0 } diff --git a/services/forms/user_form_hidden_comments.go b/services/forms/user_form_hidden_comments.go index e0c26e8ddf..35c1a6dd2a 100644 --- a/services/forms/user_form_hidden_comments.go +++ b/services/forms/user_form_hidden_comments.go @@ -7,69 +7,69 @@ package forms import ( "math/big" - "code.gitea.io/gitea/models" + issues_model "code.gitea.io/gitea/models/issues" "code.gitea.io/gitea/modules/context" "code.gitea.io/gitea/modules/log" ) -type hiddenCommentTypeGroupsType map[string][]models.CommentType +type hiddenCommentTypeGroupsType map[string][]issues_model.CommentType // hiddenCommentTypeGroups maps the group names to comment types, these group names comes from the Web UI (appearance.tmpl) var hiddenCommentTypeGroups = hiddenCommentTypeGroupsType{ "reference": { - /*3*/ models.CommentTypeIssueRef, - /*4*/ models.CommentTypeCommitRef, - /*5*/ models.CommentTypeCommentRef, - /*6*/ models.CommentTypePullRef, + /*3*/ issues_model.CommentTypeIssueRef, + /*4*/ issues_model.CommentTypeCommitRef, + /*5*/ issues_model.CommentTypeCommentRef, + /*6*/ issues_model.CommentTypePullRef, }, "label": { - /*7*/ models.CommentTypeLabel, + /*7*/ issues_model.CommentTypeLabel, }, "milestone": { - /*8*/ models.CommentTypeMilestone, + /*8*/ issues_model.CommentTypeMilestone, }, "assignee": { - /*9*/ models.CommentTypeAssignees, + /*9*/ issues_model.CommentTypeAssignees, }, "title": { - /*10*/ models.CommentTypeChangeTitle, + /*10*/ issues_model.CommentTypeChangeTitle, }, "branch": { - /*11*/ models.CommentTypeDeleteBranch, - /*25*/ models.CommentTypeChangeTargetBranch, + /*11*/ issues_model.CommentTypeDeleteBranch, + /*25*/ issues_model.CommentTypeChangeTargetBranch, }, "time_tracking": { - /*12*/ models.CommentTypeStartTracking, - /*13*/ models.CommentTypeStopTracking, - /*14*/ models.CommentTypeAddTimeManual, - /*15*/ models.CommentTypeCancelTracking, - /*26*/ models.CommentTypeDeleteTimeManual, + /*12*/ issues_model.CommentTypeStartTracking, + /*13*/ issues_model.CommentTypeStopTracking, + /*14*/ issues_model.CommentTypeAddTimeManual, + /*15*/ issues_model.CommentTypeCancelTracking, + /*26*/ issues_model.CommentTypeDeleteTimeManual, }, "deadline": { - /*16*/ models.CommentTypeAddedDeadline, - /*17*/ models.CommentTypeModifiedDeadline, - /*18*/ models.CommentTypeRemovedDeadline, + /*16*/ issues_model.CommentTypeAddedDeadline, + /*17*/ issues_model.CommentTypeModifiedDeadline, + /*18*/ issues_model.CommentTypeRemovedDeadline, }, "dependency": { - /*19*/ models.CommentTypeAddDependency, - /*20*/ models.CommentTypeRemoveDependency, + /*19*/ issues_model.CommentTypeAddDependency, + /*20*/ issues_model.CommentTypeRemoveDependency, }, "lock": { - /*23*/ models.CommentTypeLock, - /*24*/ models.CommentTypeUnlock, + /*23*/ issues_model.CommentTypeLock, + /*24*/ issues_model.CommentTypeUnlock, }, "review_request": { - /*27*/ models.CommentTypeReviewRequest, + /*27*/ issues_model.CommentTypeReviewRequest, }, "pull_request_push": { - /*29*/ models.CommentTypePullRequestPush, + /*29*/ issues_model.CommentTypePullRequestPush, }, "project": { - /*30*/ models.CommentTypeProject, - /*31*/ models.CommentTypeProjectBoard, + /*30*/ issues_model.CommentTypeProject, + /*31*/ issues_model.CommentTypeProjectBoard, }, "issue_ref": { - /*33*/ models.CommentTypeChangeIssueRef, + /*33*/ issues_model.CommentTypeChangeIssueRef, }, } diff --git a/services/gitdiff/gitdiff.go b/services/gitdiff/gitdiff.go index e56c2de8fa..97daadbc67 100644 --- a/services/gitdiff/gitdiff.go +++ b/services/gitdiff/gitdiff.go @@ -20,9 +20,9 @@ import ( "strings" "time" - "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" pull_model "code.gitea.io/gitea/models/pull" user_model "code.gitea.io/gitea/models/user" "code.gitea.io/gitea/modules/analyze" @@ -82,7 +82,7 @@ type DiffLine struct { Match int Type DiffLineType Content string - Comments []*models.Comment + Comments []*issues_model.Comment SectionInfo *DiffLineSectionInfo } @@ -704,8 +704,8 @@ type Diff struct { } // LoadComments loads comments into each line -func (diff *Diff) LoadComments(ctx context.Context, issue *models.Issue, currentUser *user_model.User) error { - allComments, err := models.FetchCodeComments(ctx, issue, currentUser) +func (diff *Diff) LoadComments(ctx context.Context, issue *issues_model.Issue, currentUser *user_model.User) error { + allComments, err := issues_model.FetchCodeComments(ctx, issue, currentUser) if err != nil { return err } @@ -1520,7 +1520,7 @@ func GetDiff(gitRepo *git.Repository, opts *DiffOptions, files ...string) (*Diff // SyncAndGetUserSpecificDiff is like GetDiff, except that user specific data such as which files the given user has already viewed on the given PR will also be set // Additionally, the database asynchronously is updated if files have changed since the last review -func SyncAndGetUserSpecificDiff(ctx context.Context, userID int64, pull *models.PullRequest, gitRepo *git.Repository, opts *DiffOptions, files ...string) (*Diff, error) { +func SyncAndGetUserSpecificDiff(ctx context.Context, userID int64, pull *issues_model.PullRequest, gitRepo *git.Repository, opts *DiffOptions, files ...string) (*Diff, error) { diff, err := GetDiff(gitRepo, opts, files...) if err != nil { return nil, err @@ -1583,7 +1583,7 @@ outer: } // CommentAsDiff returns c.Patch as *Diff -func CommentAsDiff(c *models.Comment) (*Diff, error) { +func CommentAsDiff(c *issues_model.Comment) (*Diff, error) { diff, err := ParsePatch(setting.Git.MaxGitDiffLines, setting.Git.MaxGitDiffLineCharacters, setting.Git.MaxGitDiffFiles, strings.NewReader(c.Patch), "") if err != nil { @@ -1601,7 +1601,7 @@ func CommentAsDiff(c *models.Comment) (*Diff, error) { } // CommentMustAsDiff executes AsDiff and logs the error instead of returning -func CommentMustAsDiff(c *models.Comment) *Diff { +func CommentMustAsDiff(c *issues_model.Comment) *Diff { if c == nil { return nil } diff --git a/services/gitdiff/gitdiff_test.go b/services/gitdiff/gitdiff_test.go index 3457785e5d..caca0e91d8 100644 --- a/services/gitdiff/gitdiff_test.go +++ b/services/gitdiff/gitdiff_test.go @@ -12,8 +12,8 @@ import ( "strings" "testing" - "code.gitea.io/gitea/models" "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/git" @@ -669,7 +669,7 @@ func setupDefaultDiff() *Diff { func TestDiff_LoadComments(t *testing.T) { assert.NoError(t, unittest.PrepareTestDatabase()) - issue := unittest.AssertExistsAndLoadBean(t, &models.Issue{ID: 2}).(*models.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) diff := setupDefaultDiff() assert.NoError(t, diff.LoadComments(db.DefaultContext, issue, user)) @@ -678,15 +678,15 @@ func TestDiff_LoadComments(t *testing.T) { func TestDiffLine_CanComment(t *testing.T) { assert.False(t, (&DiffLine{Type: DiffLineSection}).CanComment()) - assert.False(t, (&DiffLine{Type: DiffLineAdd, Comments: []*models.Comment{{Content: "bla"}}}).CanComment()) + assert.False(t, (&DiffLine{Type: DiffLineAdd, Comments: []*issues_model.Comment{{Content: "bla"}}}).CanComment()) assert.True(t, (&DiffLine{Type: DiffLineAdd}).CanComment()) assert.True(t, (&DiffLine{Type: DiffLineDel}).CanComment()) assert.True(t, (&DiffLine{Type: DiffLinePlain}).CanComment()) } func TestDiffLine_GetCommentSide(t *testing.T) { - assert.Equal(t, "previous", (&DiffLine{Comments: []*models.Comment{{Line: -3}}}).GetCommentSide()) - assert.Equal(t, "proposed", (&DiffLine{Comments: []*models.Comment{{Line: 3}}}).GetCommentSide()) + assert.Equal(t, "previous", (&DiffLine{Comments: []*issues_model.Comment{{Line: -3}}}).GetCommentSide()) + assert.Equal(t, "proposed", (&DiffLine{Comments: []*issues_model.Comment{{Line: 3}}}).GetCommentSide()) } func TestGetDiffRangeWithWhitespaceBehavior(t *testing.T) { diff --git a/services/gitdiff/main_test.go b/services/gitdiff/main_test.go index d4d9364ebf..17d0da6276 100644 --- a/services/gitdiff/main_test.go +++ b/services/gitdiff/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/services/issue/assignee.go b/services/issue/assignee.go index 8cad03351c..7c00f472dd 100644 --- a/services/issue/assignee.go +++ b/services/issue/assignee.go @@ -7,8 +7,8 @@ package issue import ( "context" - "code.gitea.io/gitea/models" "code.gitea.io/gitea/models/db" + 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" @@ -19,7 +19,7 @@ import ( ) // DeleteNotPassedAssignee deletes all assignees who aren't passed via the "assignees" array -func DeleteNotPassedAssignee(issue *models.Issue, doer *user_model.User, assignees []*user_model.User) (err error) { +func DeleteNotPassedAssignee(issue *issues_model.Issue, doer *user_model.User, assignees []*user_model.User) (err error) { var found bool oriAssignes := make([]*user_model.User, len(issue.Assignees)) _ = copy(oriAssignes, issue.Assignees) @@ -45,8 +45,8 @@ func DeleteNotPassedAssignee(issue *models.Issue, doer *user_model.User, assigne } // ToggleAssignee changes a user between assigned and not assigned for this issue, and make issue comment for it. -func ToggleAssignee(issue *models.Issue, doer *user_model.User, assigneeID int64) (removed bool, comment *models.Comment, err error) { - removed, comment, err = models.ToggleIssueAssignee(issue, doer, assigneeID) +func ToggleAssignee(issue *issues_model.Issue, doer *user_model.User, assigneeID int64) (removed bool, comment *issues_model.Comment, err error) { + removed, comment, err = issues_model.ToggleIssueAssignee(issue, doer, assigneeID) if err != nil { return } @@ -63,11 +63,11 @@ func ToggleAssignee(issue *models.Issue, doer *user_model.User, assigneeID int64 } // ReviewRequest add or remove a review request from a user for this PR, and make comment for it. -func ReviewRequest(issue *models.Issue, doer, reviewer *user_model.User, isAdd bool) (comment *models.Comment, err error) { +func ReviewRequest(issue *issues_model.Issue, doer, reviewer *user_model.User, isAdd bool) (comment *issues_model.Comment, err error) { if isAdd { - comment, err = models.AddReviewRequest(issue, reviewer, doer) + comment, err = issues_model.AddReviewRequest(issue, reviewer, doer) } else { - comment, err = models.RemoveReviewRequest(issue, reviewer, doer) + comment, err = issues_model.RemoveReviewRequest(issue, reviewer, doer) } if err != nil { @@ -82,16 +82,16 @@ func ReviewRequest(issue *models.Issue, doer, reviewer *user_model.User, isAdd b } // IsValidReviewRequest Check permission for ReviewRequest -func IsValidReviewRequest(ctx context.Context, reviewer, doer *user_model.User, isAdd bool, issue *models.Issue, permDoer *access_model.Permission) error { +func IsValidReviewRequest(ctx context.Context, reviewer, doer *user_model.User, isAdd bool, issue *issues_model.Issue, permDoer *access_model.Permission) error { if reviewer.IsOrganization() { - return models.ErrNotValidReviewRequest{ + return issues_model.ErrNotValidReviewRequest{ Reason: "Organization can't be added as reviewer", UserID: doer.ID, RepoID: issue.Repo.ID, } } if doer.IsOrganization() { - return models.ErrNotValidReviewRequest{ + return issues_model.ErrNotValidReviewRequest{ Reason: "Organization can't be doer to add reviewer", UserID: doer.ID, RepoID: issue.Repo.ID, @@ -111,8 +111,8 @@ func IsValidReviewRequest(ctx context.Context, reviewer, doer *user_model.User, } } - lastreview, err := models.GetReviewByIssueIDAndUserID(ctx, issue.ID, reviewer.ID) - if err != nil && !models.IsErrReviewNotExist(err) { + lastreview, err := issues_model.GetReviewByIssueIDAndUserID(ctx, issue.ID, reviewer.ID) + if err != nil && !issues_model.IsErrReviewNotExist(err) { return err } @@ -120,25 +120,25 @@ func IsValidReviewRequest(ctx context.Context, reviewer, doer *user_model.User, if isAdd { pemResult = permReviewer.CanAccessAny(perm.AccessModeRead, unit.TypePullRequests) if !pemResult { - return models.ErrNotValidReviewRequest{ + return issues_model.ErrNotValidReviewRequest{ Reason: "Reviewer can't read", UserID: doer.ID, RepoID: issue.Repo.ID, } } - if doer.ID == issue.PosterID && issue.OriginalAuthorID == 0 && lastreview != nil && lastreview.Type != models.ReviewTypeRequest { + if doer.ID == issue.PosterID && issue.OriginalAuthorID == 0 && lastreview != nil && lastreview.Type != issues_model.ReviewTypeRequest { return nil } pemResult = permDoer.CanAccessAny(perm.AccessModeWrite, unit.TypePullRequests) if !pemResult { - pemResult, err = models.IsOfficialReviewer(ctx, issue, doer) + pemResult, err = issues_model.IsOfficialReviewer(ctx, issue, doer) if err != nil { return err } if !pemResult { - return models.ErrNotValidReviewRequest{ + return issues_model.ErrNotValidReviewRequest{ Reason: "Doer can't choose reviewer", UserID: doer.ID, RepoID: issue.Repo.ID, @@ -147,20 +147,20 @@ func IsValidReviewRequest(ctx context.Context, reviewer, doer *user_model.User, } if reviewer.ID == issue.PosterID && issue.OriginalAuthorID == 0 { - return models.ErrNotValidReviewRequest{ + return issues_model.ErrNotValidReviewRequest{ Reason: "poster of pr can't be reviewer", UserID: doer.ID, RepoID: issue.Repo.ID, } } } else { - if lastreview != nil && lastreview.Type == models.ReviewTypeRequest && lastreview.ReviewerID == doer.ID { + if lastreview != nil && lastreview.Type == issues_model.ReviewTypeRequest && lastreview.ReviewerID == doer.ID { return nil } pemResult = permDoer.IsAdmin() if !pemResult { - return models.ErrNotValidReviewRequest{ + return issues_model.ErrNotValidReviewRequest{ Reason: "Doer is not admin", UserID: doer.ID, RepoID: issue.Repo.ID, @@ -172,9 +172,9 @@ func IsValidReviewRequest(ctx context.Context, reviewer, doer *user_model.User, } // IsValidTeamReviewRequest Check permission for ReviewRequest Team -func IsValidTeamReviewRequest(ctx context.Context, reviewer *organization.Team, doer *user_model.User, isAdd bool, issue *models.Issue) error { +func IsValidTeamReviewRequest(ctx context.Context, reviewer *organization.Team, doer *user_model.User, isAdd bool, issue *issues_model.Issue) error { if doer.IsOrganization() { - return models.ErrNotValidReviewRequest{ + return issues_model.ErrNotValidReviewRequest{ Reason: "Organization can't be doer to add reviewer", UserID: doer.ID, RepoID: issue.Repo.ID, @@ -192,7 +192,7 @@ func IsValidTeamReviewRequest(ctx context.Context, reviewer *organization.Team, hasTeam := organization.HasTeamRepo(ctx, reviewer.OrgID, reviewer.ID, issue.RepoID) if !hasTeam { - return models.ErrNotValidReviewRequest{ + return issues_model.ErrNotValidReviewRequest{ Reason: "Reviewing team can't read repo", UserID: doer.ID, RepoID: issue.Repo.ID, @@ -202,13 +202,13 @@ func IsValidTeamReviewRequest(ctx context.Context, reviewer *organization.Team, doerCanWrite := permission.CanAccessAny(perm.AccessModeWrite, unit.TypePullRequests) if !doerCanWrite { - official, err := models.IsOfficialReviewer(ctx, issue, doer) + official, err := issues_model.IsOfficialReviewer(ctx, issue, doer) if err != nil { log.Error("Unable to Check if IsOfficialReviewer for %-v in %-v#%d", doer, issue.Repo, issue.Index) return err } if !official { - return models.ErrNotValidReviewRequest{ + return issues_model.ErrNotValidReviewRequest{ Reason: "Doer can't choose reviewer", UserID: doer.ID, RepoID: issue.Repo.ID, @@ -216,7 +216,7 @@ func IsValidTeamReviewRequest(ctx context.Context, reviewer *organization.Team, } } } else if !permission.IsAdmin() { - return models.ErrNotValidReviewRequest{ + return issues_model.ErrNotValidReviewRequest{ Reason: "Only admin users can remove team requests. Doer is not admin", UserID: doer.ID, RepoID: issue.Repo.ID, @@ -227,11 +227,11 @@ func IsValidTeamReviewRequest(ctx context.Context, reviewer *organization.Team, } // TeamReviewRequest add or remove a review request from a team for this PR, and make comment for it. -func TeamReviewRequest(issue *models.Issue, doer *user_model.User, reviewer *organization.Team, isAdd bool) (comment *models.Comment, err error) { +func TeamReviewRequest(issue *issues_model.Issue, doer *user_model.User, reviewer *organization.Team, isAdd bool) (comment *issues_model.Comment, err error) { if isAdd { - comment, err = models.AddTeamReviewRequest(issue, reviewer, doer) + comment, err = issues_model.AddTeamReviewRequest(issue, reviewer, doer) } else { - comment, err = models.RemoveTeamReviewRequest(issue, reviewer, doer) + comment, err = issues_model.RemoveTeamReviewRequest(issue, reviewer, doer) } if err != nil { diff --git a/services/issue/assignee_test.go b/services/issue/assignee_test.go index ff4d7029eb..5c8b822499 100644 --- a/services/issue/assignee_test.go +++ b/services/issue/assignee_test.go @@ -7,8 +7,8 @@ package issue import ( "testing" - "code.gitea.io/gitea/models" "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" @@ -19,7 +19,7 @@ func TestDeleteNotPassedAssignee(t *testing.T) { assert.NoError(t, unittest.PrepareTestDatabase()) // Fake issue with assignees - issue, err := models.GetIssueWithAttrsByID(1) + issue, err := issues_model.GetIssueWithAttrsByID(1) assert.NoError(t, err) assert.EqualValues(t, 1, len(issue.Assignees)) @@ -27,7 +27,7 @@ func TestDeleteNotPassedAssignee(t *testing.T) { assert.NoError(t, err) // Check if he got removed - isAssigned, err := models.IsUserAssignedToIssue(db.DefaultContext, issue, user1) + isAssigned, err := issues_model.IsUserAssignedToIssue(db.DefaultContext, issue, user1) assert.NoError(t, err) assert.True(t, isAssigned) diff --git a/services/issue/commit.go b/services/issue/commit.go index 5140eebed1..1053a81162 100644 --- a/services/issue/commit.go +++ b/services/issue/commit.go @@ -15,6 +15,7 @@ import ( "code.gitea.io/gitea/models" "code.gitea.io/gitea/models/db" + issues_model "code.gitea.io/gitea/models/issues" access_model "code.gitea.io/gitea/models/perm/access" repo_model "code.gitea.io/gitea/models/repo" user_model "code.gitea.io/gitea/models/user" @@ -76,22 +77,22 @@ func timeLogToAmount(str string) int64 { return a } -func issueAddTime(issue *models.Issue, doer *user_model.User, time time.Time, timeLog string) error { +func issueAddTime(issue *issues_model.Issue, doer *user_model.User, time time.Time, timeLog string) error { amount := timeLogToAmount(timeLog) if amount == 0 { return nil } - _, err := models.AddTime(doer, issue, amount, time) + _, err := issues_model.AddTime(doer, issue, amount, time) return err } // getIssueFromRef returns the issue referenced by a ref. Returns a nil *Issue // if the provided ref references a non-existent issue. -func getIssueFromRef(repo *repo_model.Repository, index int64) (*models.Issue, error) { - issue, err := models.GetIssueByIndex(repo.ID, index) +func getIssueFromRef(repo *repo_model.Repository, index int64) (*issues_model.Issue, error) { + issue, err := issues_model.GetIssueByIndex(repo.ID, index) if err != nil { - if models.IsErrIssueNotExist(err) { + if issues_model.IsErrIssueNotExist(err) { return nil, nil } return nil, err @@ -112,7 +113,7 @@ func UpdateIssuesCommit(doer *user_model.User, repo *repo_model.Repository, comm refMarked := make(map[markKey]bool) var refRepo *repo_model.Repository - var refIssue *models.Issue + var refIssue *issues_model.Issue var err error for _, ref := range references.FindAllIssueReferences(c.Message) { @@ -153,7 +154,7 @@ func UpdateIssuesCommit(doer *user_model.User, repo *repo_model.Repository, comm } message := fmt.Sprintf(`%s`, html.EscapeString(repo.Link()), html.EscapeString(url.PathEscape(c.Sha1)), html.EscapeString(strings.SplitN(c.Message, "\n", 2)[0])) - if err = models.CreateRefComment(doer, refRepo, refIssue, message, c.Sha1); err != nil { + if err = issues_model.CreateRefComment(doer, refRepo, refIssue, message, c.Sha1); err != nil { return err } diff --git a/services/issue/commit_test.go b/services/issue/commit_test.go index 37283a7890..ce3f913627 100644 --- a/services/issue/commit_test.go +++ b/services/issue/commit_test.go @@ -8,6 +8,7 @@ import ( "testing" "code.gitea.io/gitea/models" + 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" @@ -50,16 +51,16 @@ func TestUpdateIssuesCommit(t *testing.T) { repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 1}).(*repo_model.Repository) repo.Owner = user - commentBean := &models.Comment{ - Type: models.CommentTypeCommitRef, + commentBean := &issues_model.Comment{ + Type: issues_model.CommentTypeCommitRef, CommitSHA: "abcdef1", PosterID: user.ID, IssueID: 1, } - issueBean := &models.Issue{RepoID: repo.ID, Index: 4} + issueBean := &issues_model.Issue{RepoID: repo.ID, Index: 4} unittest.AssertNotExistsBean(t, commentBean) - unittest.AssertNotExistsBean(t, &models.Issue{RepoID: repo.ID, Index: 2}, "is_closed=1") + unittest.AssertNotExistsBean(t, &issues_model.Issue{RepoID: repo.ID, Index: 2}, "is_closed=1") assert.NoError(t, UpdateIssuesCommit(user, repo, pushCommits, repo.DefaultBranch)) unittest.AssertExistsAndLoadBean(t, commentBean) unittest.AssertExistsAndLoadBean(t, issueBean, "is_closed=1") @@ -77,16 +78,16 @@ func TestUpdateIssuesCommit(t *testing.T) { }, } repo = unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 3}).(*repo_model.Repository) - commentBean = &models.Comment{ - Type: models.CommentTypeCommitRef, + commentBean = &issues_model.Comment{ + Type: issues_model.CommentTypeCommitRef, CommitSHA: "abcdef1", PosterID: user.ID, IssueID: 6, } - issueBean = &models.Issue{RepoID: repo.ID, Index: 1} + issueBean = &issues_model.Issue{RepoID: repo.ID, Index: 1} unittest.AssertNotExistsBean(t, commentBean) - unittest.AssertNotExistsBean(t, &models.Issue{RepoID: repo.ID, Index: 1}, "is_closed=1") + unittest.AssertNotExistsBean(t, &issues_model.Issue{RepoID: repo.ID, Index: 1}, "is_closed=1") assert.NoError(t, UpdateIssuesCommit(user, repo, pushCommits, "non-existing-branch")) unittest.AssertExistsAndLoadBean(t, commentBean) unittest.AssertNotExistsBean(t, issueBean, "is_closed=1") @@ -103,16 +104,16 @@ func TestUpdateIssuesCommit(t *testing.T) { }, } repo = unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 3}).(*repo_model.Repository) - commentBean = &models.Comment{ - Type: models.CommentTypeCommitRef, + commentBean = &issues_model.Comment{ + Type: issues_model.CommentTypeCommitRef, CommitSHA: "abcdef3", PosterID: user.ID, IssueID: 6, } - issueBean = &models.Issue{RepoID: repo.ID, Index: 1} + issueBean = &issues_model.Issue{RepoID: repo.ID, Index: 1} unittest.AssertNotExistsBean(t, commentBean) - unittest.AssertNotExistsBean(t, &models.Issue{RepoID: repo.ID, Index: 1}, "is_closed=1") + unittest.AssertNotExistsBean(t, &issues_model.Issue{RepoID: repo.ID, Index: 1}, "is_closed=1") assert.NoError(t, UpdateIssuesCommit(user, repo, pushCommits, repo.DefaultBranch)) unittest.AssertExistsAndLoadBean(t, commentBean) unittest.AssertExistsAndLoadBean(t, issueBean, "is_closed=1") @@ -136,9 +137,9 @@ func TestUpdateIssuesCommit_Colon(t *testing.T) { repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 1}).(*repo_model.Repository) repo.Owner = user - issueBean := &models.Issue{RepoID: repo.ID, Index: 4} + issueBean := &issues_model.Issue{RepoID: repo.ID, Index: 4} - unittest.AssertNotExistsBean(t, &models.Issue{RepoID: repo.ID, Index: 2}, "is_closed=1") + unittest.AssertNotExistsBean(t, &issues_model.Issue{RepoID: repo.ID, Index: 2}, "is_closed=1") assert.NoError(t, UpdateIssuesCommit(user, repo, pushCommits, repo.DefaultBranch)) unittest.AssertExistsAndLoadBean(t, issueBean, "is_closed=1") unittest.CheckConsistencyFor(t, &models.Action{}) @@ -161,14 +162,14 @@ func TestUpdateIssuesCommit_Issue5957(t *testing.T) { } repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 2}).(*repo_model.Repository) - commentBean := &models.Comment{ - Type: models.CommentTypeCommitRef, + commentBean := &issues_model.Comment{ + Type: issues_model.CommentTypeCommitRef, CommitSHA: "abcdef1", PosterID: user.ID, IssueID: 7, } - issueBean := &models.Issue{RepoID: repo.ID, Index: 2, ID: 7} + issueBean := &issues_model.Issue{RepoID: repo.ID, Index: 2, ID: 7} unittest.AssertNotExistsBean(t, commentBean) unittest.AssertNotExistsBean(t, issueBean, "is_closed=1") @@ -196,14 +197,14 @@ func TestUpdateIssuesCommit_AnotherRepo(t *testing.T) { } repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 2}).(*repo_model.Repository) - commentBean := &models.Comment{ - Type: models.CommentTypeCommitRef, + commentBean := &issues_model.Comment{ + Type: issues_model.CommentTypeCommitRef, CommitSHA: "abcdef1", PosterID: user.ID, IssueID: 1, } - issueBean := &models.Issue{RepoID: 1, Index: 1, ID: 1} + issueBean := &issues_model.Issue{RepoID: 1, Index: 1, ID: 1} unittest.AssertNotExistsBean(t, commentBean) unittest.AssertNotExistsBean(t, issueBean, "is_closed=1") @@ -231,14 +232,14 @@ func TestUpdateIssuesCommit_AnotherRepo_FullAddress(t *testing.T) { } repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 2}).(*repo_model.Repository) - commentBean := &models.Comment{ - Type: models.CommentTypeCommitRef, + commentBean := &issues_model.Comment{ + Type: issues_model.CommentTypeCommitRef, CommitSHA: "abcdef1", PosterID: user.ID, IssueID: 1, } - issueBean := &models.Issue{RepoID: 1, Index: 1, ID: 1} + issueBean := &issues_model.Issue{RepoID: 1, Index: 1, ID: 1} unittest.AssertNotExistsBean(t, commentBean) unittest.AssertNotExistsBean(t, issueBean, "is_closed=1") @@ -274,20 +275,20 @@ func TestUpdateIssuesCommit_AnotherRepoNoPermission(t *testing.T) { } repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 6}).(*repo_model.Repository) - commentBean := &models.Comment{ - Type: models.CommentTypeCommitRef, + commentBean := &issues_model.Comment{ + Type: issues_model.CommentTypeCommitRef, CommitSHA: "abcdef3", PosterID: user.ID, IssueID: 6, } - commentBean2 := &models.Comment{ - Type: models.CommentTypeCommitRef, + commentBean2 := &issues_model.Comment{ + Type: issues_model.CommentTypeCommitRef, CommitSHA: "abcdef4", PosterID: user.ID, IssueID: 6, } - issueBean := &models.Issue{RepoID: 3, Index: 1, ID: 6} + issueBean := &issues_model.Issue{RepoID: 3, Index: 1, ID: 6} unittest.AssertNotExistsBean(t, commentBean) unittest.AssertNotExistsBean(t, commentBean2) diff --git a/services/issue/content.go b/services/issue/content.go index a60878479b..6f493892f4 100644 --- a/services/issue/content.go +++ b/services/issue/content.go @@ -5,16 +5,16 @@ package issue import ( - "code.gitea.io/gitea/models" + issues_model "code.gitea.io/gitea/models/issues" user_model "code.gitea.io/gitea/models/user" "code.gitea.io/gitea/modules/notification" ) // ChangeContent changes issue content, as the given user. -func ChangeContent(issue *models.Issue, doer *user_model.User, content string) (err error) { +func ChangeContent(issue *issues_model.Issue, doer *user_model.User, content string) (err error) { oldContent := issue.Content - if err := models.ChangeIssueContent(issue, doer, content); err != nil { + if err := issues_model.ChangeIssueContent(issue, doer, content); err != nil { return err } diff --git a/services/issue/issue.go b/services/issue/issue.go index 78a486727a..ded281e209 100644 --- a/services/issue/issue.go +++ b/services/issue/issue.go @@ -8,18 +8,22 @@ import ( "fmt" "code.gitea.io/gitea/models" + admin_model "code.gitea.io/gitea/models/admin" "code.gitea.io/gitea/models/db" + issues_model "code.gitea.io/gitea/models/issues" access_model "code.gitea.io/gitea/models/perm/access" + project_model "code.gitea.io/gitea/models/project" repo_model "code.gitea.io/gitea/models/repo" user_model "code.gitea.io/gitea/models/user" "code.gitea.io/gitea/modules/git" "code.gitea.io/gitea/modules/notification" + "code.gitea.io/gitea/modules/storage" "code.gitea.io/gitea/modules/util" ) // NewIssue creates new issue with labels for repository. -func NewIssue(repo *repo_model.Repository, issue *models.Issue, labelIDs []int64, uuids []string, assigneeIDs []int64) error { - if err := models.NewIssue(repo, issue, labelIDs, uuids); err != nil { +func NewIssue(repo *repo_model.Repository, issue *issues_model.Issue, labelIDs []int64, uuids []string, assigneeIDs []int64) error { + if err := issues_model.NewIssue(repo, issue, labelIDs, uuids); err != nil { return err } @@ -29,7 +33,7 @@ func NewIssue(repo *repo_model.Repository, issue *models.Issue, labelIDs []int64 } } - mentions, err := models.FindAndUpdateIssueMentions(db.DefaultContext, issue, issue.Poster, issue.Content) + mentions, err := issues_model.FindAndUpdateIssueMentions(db.DefaultContext, issue, issue.Poster, issue.Content) if err != nil { return err } @@ -46,11 +50,11 @@ func NewIssue(repo *repo_model.Repository, issue *models.Issue, labelIDs []int64 } // ChangeTitle changes the title of this issue, as the given user. -func ChangeTitle(issue *models.Issue, doer *user_model.User, title string) (err error) { +func ChangeTitle(issue *issues_model.Issue, doer *user_model.User, title string) (err error) { oldTitle := issue.Title issue.Title = title - if err = models.ChangeIssueTitle(issue, doer, oldTitle); err != nil { + if err = issues_model.ChangeIssueTitle(issue, doer, oldTitle); err != nil { return } @@ -60,11 +64,11 @@ func ChangeTitle(issue *models.Issue, doer *user_model.User, title string) (err } // ChangeIssueRef changes the branch of this issue, as the given user. -func ChangeIssueRef(issue *models.Issue, doer *user_model.User, ref string) error { +func ChangeIssueRef(issue *issues_model.Issue, doer *user_model.User, ref string) error { oldRef := issue.Ref issue.Ref = ref - if err := models.ChangeIssueRef(issue, doer, oldRef); err != nil { + if err := issues_model.ChangeIssueRef(issue, doer, oldRef); err != nil { return err } @@ -79,7 +83,7 @@ func ChangeIssueRef(issue *models.Issue, doer *user_model.User, ref string) erro // "assignees" (array): Logins for Users to assign to this issue. // Pass one or more user logins to replace the set of assignees on this Issue. // Send an empty array ([]) to clear all assignees from the Issue. -func UpdateAssignees(issue *models.Issue, oneAssignee string, multipleAssignees []string, doer *user_model.User) (err error) { +func UpdateAssignees(issue *issues_model.Issue, oneAssignee string, multipleAssignees []string, doer *user_model.User) (err error) { var allNewAssignees []*user_model.User // Keep the old assignee thingy for compatibility reasons @@ -129,9 +133,9 @@ func UpdateAssignees(issue *models.Issue, oneAssignee string, multipleAssignees } // DeleteIssue deletes an issue -func DeleteIssue(doer *user_model.User, gitRepo *git.Repository, issue *models.Issue) error { +func DeleteIssue(doer *user_model.User, gitRepo *git.Repository, issue *issues_model.Issue) error { // load issue before deleting it - if err := issue.LoadAttributes(); err != nil { + if err := issue.LoadAttributes(db.DefaultContext); err != nil { return err } if err := issue.LoadPullRequest(); err != nil { @@ -139,7 +143,7 @@ func DeleteIssue(doer *user_model.User, gitRepo *git.Repository, issue *models.I } // delete entries in database - if err := models.DeleteIssue(issue); err != nil { + if err := deleteIssue(issue); err != nil { return err } @@ -157,14 +161,14 @@ func DeleteIssue(doer *user_model.User, gitRepo *git.Repository, issue *models.I // AddAssigneeIfNotAssigned adds an assignee only if he isn't already assigned to the issue. // Also checks for access of assigned user -func AddAssigneeIfNotAssigned(issue *models.Issue, doer *user_model.User, assigneeID int64) (err error) { +func AddAssigneeIfNotAssigned(issue *issues_model.Issue, doer *user_model.User, assigneeID int64) (err error) { assignee, err := user_model.GetUserByID(assigneeID) if err != nil { return err } // Check if the user is already assigned - isAssigned, err := models.IsUserAssignedToIssue(db.DefaultContext, issue, assignee) + isAssigned, err := issues_model.IsUserAssignedToIssue(db.DefaultContext, issue, assignee) if err != nil { return err } @@ -178,7 +182,7 @@ func AddAssigneeIfNotAssigned(issue *models.Issue, doer *user_model.User, assign return err } if !valid { - return models.ErrUserDoesNotHaveAccessToRepo{UserID: assigneeID, RepoName: issue.Repo.Name} + return repo_model.ErrUserDoesNotHaveAccessToRepo{UserID: assigneeID, RepoName: issue.Repo.Name} } _, _, err = ToggleAssignee(issue, doer, assigneeID) @@ -191,7 +195,7 @@ func AddAssigneeIfNotAssigned(issue *models.Issue, doer *user_model.User, assign // GetRefEndNamesAndURLs retrieves the ref end names (e.g. refs/heads/branch-name -> branch-name) // and their respective URLs. -func GetRefEndNamesAndURLs(issues []*models.Issue, repoLink string) (map[int64]string, map[int64]string) { +func GetRefEndNamesAndURLs(issues []*issues_model.Issue, repoLink string) (map[int64]string, map[int64]string) { issueRefEndNames := make(map[int64]string, len(issues)) issueRefURLs := make(map[int64]string, len(issues)) for _, issue := range issues { @@ -202,3 +206,77 @@ func GetRefEndNamesAndURLs(issues []*models.Issue, repoLink string) (map[int64]s } return issueRefEndNames, issueRefURLs } + +// deleteIssue deletes the issue +func deleteIssue(issue *issues_model.Issue) error { + ctx, committer, err := db.TxContext() + if err != nil { + return err + } + defer committer.Close() + + e := db.GetEngine(ctx) + if _, err := e.ID(issue.ID).NoAutoCondition().Delete(issue); err != nil { + return err + } + + if err := repo_model.UpdateRepoIssueNumbers(ctx, issue.RepoID, issue.IsPull, issue.IsClosed); err != nil { + return err + } + + if err := models.DeleteIssueActions(ctx, issue.RepoID, issue.ID); err != nil { + return err + } + + // find attachments related to this issue and remove them + if err := issue.LoadAttributes(ctx); err != nil { + return err + } + + for i := range issue.Attachments { + admin_model.RemoveStorageWithNotice(ctx, storage.Attachments, "Delete issue attachment", issue.Attachments[i].RelativePath()) + } + + // delete all database data still assigned to this issue + if err := issues_model.DeleteInIssue(ctx, issue.ID, + &issues_model.ContentHistory{}, + &issues_model.Comment{}, + &issues_model.IssueLabel{}, + &issues_model.IssueDependency{}, + &issues_model.IssueAssignees{}, + &issues_model.IssueUser{}, + &models.Notification{}, + &issues_model.Reaction{}, + &issues_model.IssueWatch{}, + &issues_model.Stopwatch{}, + &issues_model.TrackedTime{}, + &project_model.ProjectIssue{}, + &repo_model.Attachment{}, + &issues_model.PullRequest{}, + ); err != nil { + return err + } + + // References to this issue in other issues + if _, err := db.DeleteByBean(ctx, &issues_model.Comment{ + RefIssueID: issue.ID, + }); err != nil { + return err + } + + // Delete dependencies for issues in other repositories + if _, err := db.DeleteByBean(ctx, &issues_model.IssueDependency{ + DependencyID: issue.ID, + }); err != nil { + return err + } + + // delete from dependent issues + if _, err := db.DeleteByBean(ctx, &issues_model.Comment{ + DependentIssueID: issue.ID, + }); err != nil { + return err + } + + return committer.Commit() +} diff --git a/services/issue/issue_test.go b/services/issue/issue_test.go index caae773616..20f3a3296c 100644 --- a/services/issue/issue_test.go +++ b/services/issue/issue_test.go @@ -7,13 +7,17 @@ package issue import ( "testing" - "code.gitea.io/gitea/models" + "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" ) func TestGetRefEndNamesAndURLs(t *testing.T) { - issues := []*models.Issue{ + issues := []*issues_model.Issue{ {ID: 1, Ref: "refs/heads/branch1"}, {ID: 2, Ref: "refs/tags/tag1"}, {ID: 3, Ref: "c0ffee"}, @@ -28,3 +32,56 @@ func TestGetRefEndNamesAndURLs(t *testing.T) { 3: repoLink + "/src/commit/c0ffee", }, urls) } + +func TestIssue_DeleteIssue(t *testing.T) { + assert.NoError(t, unittest.PrepareTestDatabase()) + + issueIDs, err := issues_model.GetIssueIDsByRepoID(db.DefaultContext, 1) + assert.NoError(t, err) + assert.EqualValues(t, 5, len(issueIDs)) + + issue := &issues_model.Issue{ + RepoID: 1, + ID: issueIDs[2], + } + + err = deleteIssue(issue) + assert.NoError(t, err) + issueIDs, err = issues_model.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 = issues_model.GetIssueByID(db.DefaultContext, 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 := 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.CreateIssueDependency(user, issue1, issue2) + assert.NoError(t, err) + left, err := issues_model.IssueNoDependenciesLeft(db.DefaultContext, issue1) + assert.NoError(t, err) + assert.False(t, left) + + err = deleteIssue(issue2) + assert.NoError(t, err) + left, err = issues_model.IssueNoDependenciesLeft(db.DefaultContext, issue1) + assert.NoError(t, err) + assert.True(t, left) +} diff --git a/services/issue/label.go b/services/issue/label.go index 289466f604..bc5f9b910e 100644 --- a/services/issue/label.go +++ b/services/issue/label.go @@ -5,16 +5,16 @@ package issue import ( - "code.gitea.io/gitea/models" "code.gitea.io/gitea/models/db" + issues_model "code.gitea.io/gitea/models/issues" access_model "code.gitea.io/gitea/models/perm/access" user_model "code.gitea.io/gitea/models/user" "code.gitea.io/gitea/modules/notification" ) // ClearLabels clears all of an issue's labels -func ClearLabels(issue *models.Issue, doer *user_model.User) (err error) { - if err = models.ClearIssueLabels(issue, doer); err != nil { +func ClearLabels(issue *issues_model.Issue, doer *user_model.User) (err error) { + if err = issues_model.ClearIssueLabels(issue, doer); err != nil { return } @@ -24,18 +24,18 @@ func ClearLabels(issue *models.Issue, doer *user_model.User) (err error) { } // AddLabel adds a new label to the issue. -func AddLabel(issue *models.Issue, doer *user_model.User, label *models.Label) error { - if err := models.NewIssueLabel(issue, label, doer); err != nil { +func AddLabel(issue *issues_model.Issue, doer *user_model.User, label *issues_model.Label) error { + if err := issues_model.NewIssueLabel(issue, label, doer); err != nil { return err } - notification.NotifyIssueChangeLabels(doer, issue, []*models.Label{label}, nil) + notification.NotifyIssueChangeLabels(doer, issue, []*issues_model.Label{label}, nil) return nil } // AddLabels adds a list of new labels to the issue. -func AddLabels(issue *models.Issue, doer *user_model.User, labels []*models.Label) error { - if err := models.NewIssueLabels(issue, labels, doer); err != nil { +func AddLabels(issue *issues_model.Issue, doer *user_model.User, labels []*issues_model.Label) error { + if err := issues_model.NewIssueLabels(issue, labels, doer); err != nil { return err } @@ -44,7 +44,7 @@ func AddLabels(issue *models.Issue, doer *user_model.User, labels []*models.Labe } // RemoveLabel removes a label from issue by given ID. -func RemoveLabel(issue *models.Issue, doer *user_model.User, label *models.Label) error { +func RemoveLabel(issue *issues_model.Issue, doer *user_model.User, label *issues_model.Label) error { ctx, committer, err := db.TxContext() if err != nil { return err @@ -61,12 +61,12 @@ func RemoveLabel(issue *models.Issue, doer *user_model.User, label *models.Label } if !perm.CanWriteIssuesOrPulls(issue.IsPull) { if label.OrgID > 0 { - return models.ErrOrgLabelNotExist{} + return issues_model.ErrOrgLabelNotExist{} } - return models.ErrRepoLabelNotExist{} + return issues_model.ErrRepoLabelNotExist{} } - if err := models.DeleteIssueLabel(ctx, issue, label, doer); err != nil { + if err := issues_model.DeleteIssueLabel(ctx, issue, label, doer); err != nil { return err } @@ -74,18 +74,18 @@ func RemoveLabel(issue *models.Issue, doer *user_model.User, label *models.Label return err } - notification.NotifyIssueChangeLabels(doer, issue, nil, []*models.Label{label}) + notification.NotifyIssueChangeLabels(doer, issue, nil, []*issues_model.Label{label}) return nil } // ReplaceLabels removes all current labels and add new labels to the issue. -func ReplaceLabels(issue *models.Issue, doer *user_model.User, labels []*models.Label) error { - old, err := models.GetLabelsByIssueID(db.DefaultContext, issue.ID) +func ReplaceLabels(issue *issues_model.Issue, doer *user_model.User, labels []*issues_model.Label) error { + old, err := issues_model.GetLabelsByIssueID(db.DefaultContext, issue.ID) if err != nil { return err } - if err := models.ReplaceIssueLabels(issue, labels, doer); err != nil { + if err := issues_model.ReplaceIssueLabels(issue, labels, doer); err != nil { return err } diff --git a/services/issue/label_test.go b/services/issue/label_test.go index 73e30e894f..120c9ea4f1 100644 --- a/services/issue/label_test.go +++ b/services/issue/label_test.go @@ -7,7 +7,7 @@ package issue import ( "testing" - "code.gitea.io/gitea/models" + issues_model "code.gitea.io/gitea/models/issues" "code.gitea.io/gitea/models/unittest" user_model "code.gitea.io/gitea/models/user" @@ -27,15 +27,15 @@ func TestIssue_AddLabels(t *testing.T) { } for _, test := range tests { assert.NoError(t, unittest.PrepareTestDatabase()) - issue := unittest.AssertExistsAndLoadBean(t, &models.Issue{ID: test.issueID}).(*models.Issue) - labels := make([]*models.Label, len(test.labelIDs)) + issue := unittest.AssertExistsAndLoadBean(t, &issues_model.Issue{ID: test.issueID}).(*issues_model.Issue) + labels := make([]*issues_model.Label, len(test.labelIDs)) for i, labelID := range test.labelIDs { - labels[i] = unittest.AssertExistsAndLoadBean(t, &models.Label{ID: labelID}).(*models.Label) + labels[i] = unittest.AssertExistsAndLoadBean(t, &issues_model.Label{ID: labelID}).(*issues_model.Label) } doer := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: test.doerID}).(*user_model.User) assert.NoError(t, AddLabels(issue, doer, labels)) for _, labelID := range test.labelIDs { - unittest.AssertExistsAndLoadBean(t, &models.IssueLabel{IssueID: test.issueID, LabelID: labelID}) + unittest.AssertExistsAndLoadBean(t, &issues_model.IssueLabel{IssueID: test.issueID, LabelID: labelID}) } } } @@ -53,10 +53,10 @@ func TestIssue_AddLabel(t *testing.T) { } for _, test := range tests { assert.NoError(t, unittest.PrepareTestDatabase()) - issue := unittest.AssertExistsAndLoadBean(t, &models.Issue{ID: test.issueID}).(*models.Issue) - label := unittest.AssertExistsAndLoadBean(t, &models.Label{ID: test.labelID}).(*models.Label) + issue := unittest.AssertExistsAndLoadBean(t, &issues_model.Issue{ID: test.issueID}).(*issues_model.Issue) + label := unittest.AssertExistsAndLoadBean(t, &issues_model.Label{ID: test.labelID}).(*issues_model.Label) doer := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: test.doerID}).(*user_model.User) assert.NoError(t, AddLabel(issue, doer, label)) - unittest.AssertExistsAndLoadBean(t, &models.IssueLabel{IssueID: test.issueID, LabelID: test.labelID}) + unittest.AssertExistsAndLoadBean(t, &issues_model.IssueLabel{IssueID: test.issueID, LabelID: test.labelID}) } } diff --git a/services/issue/milestone.go b/services/issue/milestone.go index 287f8ae285..af337c3f14 100644 --- a/services/issue/milestone.go +++ b/services/issue/milestone.go @@ -8,15 +8,14 @@ import ( "context" "fmt" - "code.gitea.io/gitea/models" "code.gitea.io/gitea/models/db" issues_model "code.gitea.io/gitea/models/issues" user_model "code.gitea.io/gitea/models/user" "code.gitea.io/gitea/modules/notification" ) -func changeMilestoneAssign(ctx context.Context, doer *user_model.User, issue *models.Issue, oldMilestoneID int64) error { - if err := models.UpdateIssueCols(ctx, issue, "milestone_id"); err != nil { +func changeMilestoneAssign(ctx context.Context, doer *user_model.User, issue *issues_model.Issue, oldMilestoneID int64) error { + if err := issues_model.UpdateIssueCols(ctx, issue, "milestone_id"); err != nil { return err } @@ -37,15 +36,15 @@ func changeMilestoneAssign(ctx context.Context, doer *user_model.User, issue *mo return err } - opts := &models.CreateCommentOptions{ - Type: models.CommentTypeMilestone, + opts := &issues_model.CreateCommentOptions{ + Type: issues_model.CommentTypeMilestone, Doer: doer, Repo: issue.Repo, Issue: issue, OldMilestoneID: oldMilestoneID, MilestoneID: issue.MilestoneID, } - if _, err := models.CreateCommentCtx(ctx, opts); err != nil { + if _, err := issues_model.CreateCommentCtx(ctx, opts); err != nil { return err } } @@ -54,7 +53,7 @@ func changeMilestoneAssign(ctx context.Context, doer *user_model.User, issue *mo } // ChangeMilestoneAssign changes assignment of milestone for issue. -func ChangeMilestoneAssign(issue *models.Issue, doer *user_model.User, oldMilestoneID int64) (err error) { +func ChangeMilestoneAssign(issue *issues_model.Issue, doer *user_model.User, oldMilestoneID int64) (err error) { ctx, committer, err := db.TxContext() if err != nil { return err diff --git a/services/issue/milestone_test.go b/services/issue/milestone_test.go index 80e37a8acd..d08b1ae8c7 100644 --- a/services/issue/milestone_test.go +++ b/services/issue/milestone_test.go @@ -7,7 +7,6 @@ package issue import ( "testing" - "code.gitea.io/gitea/models" issues_model "code.gitea.io/gitea/models/issues" "code.gitea.io/gitea/models/unittest" user_model "code.gitea.io/gitea/models/user" @@ -17,7 +16,7 @@ import ( func TestChangeMilestoneAssign(t *testing.T) { assert.NoError(t, unittest.PrepareTestDatabase()) - issue := unittest.AssertExistsAndLoadBean(t, &models.Issue{RepoID: 1}).(*models.Issue) + issue := unittest.AssertExistsAndLoadBean(t, &issues_model.Issue{RepoID: 1}).(*issues_model.Issue) doer := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2}).(*user_model.User) assert.NotNil(t, issue) assert.NotNil(t, doer) @@ -25,11 +24,11 @@ func TestChangeMilestoneAssign(t *testing.T) { oldMilestoneID := issue.MilestoneID issue.MilestoneID = 2 assert.NoError(t, ChangeMilestoneAssign(issue, doer, oldMilestoneID)) - unittest.AssertExistsAndLoadBean(t, &models.Comment{ + unittest.AssertExistsAndLoadBean(t, &issues_model.Comment{ IssueID: issue.ID, - Type: models.CommentTypeMilestone, + Type: issues_model.CommentTypeMilestone, MilestoneID: issue.MilestoneID, OldMilestoneID: oldMilestoneID, }) - unittest.CheckConsistencyFor(t, &issues_model.Milestone{}, &models.Issue{}) + unittest.CheckConsistencyFor(t, &issues_model.Milestone{}, &issues_model.Issue{}) } diff --git a/services/issue/status.go b/services/issue/status.go index d2b4fc303e..0da5c88762 100644 --- a/services/issue/status.go +++ b/services/issue/status.go @@ -7,25 +7,25 @@ package issue import ( "context" - "code.gitea.io/gitea/models" "code.gitea.io/gitea/models/db" + issues_model "code.gitea.io/gitea/models/issues" user_model "code.gitea.io/gitea/models/user" "code.gitea.io/gitea/modules/log" "code.gitea.io/gitea/modules/notification" ) // ChangeStatus changes issue status to open or closed. -func ChangeStatus(issue *models.Issue, doer *user_model.User, closed bool) error { +func ChangeStatus(issue *issues_model.Issue, doer *user_model.User, closed bool) error { return changeStatusCtx(db.DefaultContext, issue, doer, closed) } // changeStatusCtx changes issue status to open or closed. // TODO: if context is not db.DefaultContext we get a deadlock!!! -func changeStatusCtx(ctx context.Context, issue *models.Issue, doer *user_model.User, closed bool) error { - comment, err := models.ChangeIssueStatus(ctx, issue, doer, closed) +func changeStatusCtx(ctx context.Context, issue *issues_model.Issue, doer *user_model.User, closed bool) error { + comment, err := issues_model.ChangeIssueStatus(ctx, issue, doer, closed) if err != nil { - if models.IsErrDependenciesLeft(err) && closed { - if err := models.FinishIssueStopwatchIfPossible(ctx, doer, issue); err != nil { + if issues_model.IsErrDependenciesLeft(err) && closed { + if err := issues_model.FinishIssueStopwatchIfPossible(ctx, doer, issue); err != nil { log.Error("Unable to stop stopwatch for issue[%d]#%d: %v", issue.ID, issue.Index, err) } } @@ -33,7 +33,7 @@ func changeStatusCtx(ctx context.Context, issue *models.Issue, doer *user_model. } if closed { - if err := models.FinishIssueStopwatchIfPossible(ctx, doer, issue); err != nil { + if err := issues_model.FinishIssueStopwatchIfPossible(ctx, doer, issue); err != nil { return err } } diff --git a/services/mailer/mail.go b/services/mailer/mail.go index bdd7e25cab..81cfb2e31a 100644 --- a/services/mailer/mail.go +++ b/services/mailer/mail.go @@ -19,6 +19,7 @@ import ( "code.gitea.io/gitea/models" "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/base" @@ -220,10 +221,10 @@ func composeIssueCommentMessages(ctx *mailCommentContext, lang string, recipient prefix string // Fall back subject for bad templates, make sure subject is never empty fallback string - reviewComments []*models.Comment + reviewComments []*issues_model.Comment ) - commentType := models.CommentTypeComment + commentType := issues_model.CommentTypeComment if ctx.Comment != nil { commentType = ctx.Comment.Type link = ctx.Issue.HTMLURL() + "#" + ctx.Comment.HashTag() @@ -231,7 +232,7 @@ func composeIssueCommentMessages(ctx *mailCommentContext, lang string, recipient link = ctx.Issue.HTMLURL() } - reviewType := models.ReviewTypeComment + reviewType := issues_model.ReviewTypeComment if ctx.Comment != nil && ctx.Comment.Review != nil { reviewType = ctx.Comment.Review.Type } @@ -254,7 +255,7 @@ func composeIssueCommentMessages(ctx *mailCommentContext, lang string, recipient fallback = prefix + fallbackMailSubject(ctx.Issue) if ctx.Comment != nil && ctx.Comment.Review != nil { - reviewComments = make([]*models.Comment, 0, 10) + reviewComments = make([]*issues_model.Comment, 0, 10) for _, lines := range ctx.Comment.Review.CodeComments { for _, comments := range lines { reviewComments = append(reviewComments, comments...) @@ -328,7 +329,7 @@ func composeIssueCommentMessages(ctx *mailCommentContext, lang string, recipient return msgs, nil } -func createReference(issue *models.Issue, comment *models.Comment, actionType models.ActionType) string { +func createReference(issue *issues_model.Issue, comment *issues_model.Comment, actionType models.ActionType) string { var path string if issue.IsPull { path = "pulls" @@ -400,7 +401,7 @@ func sanitizeSubject(subject string) string { } // SendIssueAssignedMail composes and sends issue assigned email -func SendIssueAssignedMail(issue *models.Issue, doer *user_model.User, content string, comment *models.Comment, recipients []*user_model.User) error { +func SendIssueAssignedMail(issue *issues_model.Issue, doer *user_model.User, content string, comment *issues_model.Comment, recipients []*user_model.User) error { if setting.MailService == nil { // No mail service configured return nil @@ -439,8 +440,8 @@ func SendIssueAssignedMail(issue *models.Issue, doer *user_model.User, content s // actionToTemplate returns the type and name of the action facing the user // (slightly different from models.ActionType) and the name of the template to use (based on availability) -func actionToTemplate(issue *models.Issue, actionType models.ActionType, - commentType models.CommentType, reviewType models.ReviewType, +func actionToTemplate(issue *issues_model.Issue, actionType models.ActionType, + commentType issues_model.CommentType, reviewType issues_model.ReviewType, ) (typeName, name, template string) { if issue.IsPull { typeName = "pull" @@ -464,20 +465,20 @@ func actionToTemplate(issue *models.Issue, actionType models.ActionType, name = "ready_for_review" default: switch commentType { - case models.CommentTypeReview: + case issues_model.CommentTypeReview: switch reviewType { - case models.ReviewTypeApprove: + case issues_model.ReviewTypeApprove: name = "approve" - case models.ReviewTypeReject: + case issues_model.ReviewTypeReject: name = "reject" default: name = "review" } - case models.CommentTypeCode: + case issues_model.CommentTypeCode: name = "code" - case models.CommentTypeAssignees: + case issues_model.CommentTypeAssignees: name = "assigned" - case models.CommentTypePullRequestPush: + case issues_model.CommentTypePullRequestPush: name = "push" default: name = "default" diff --git a/services/mailer/mail_comment.go b/services/mailer/mail_comment.go index baecd2a101..95d11ae8a1 100644 --- a/services/mailer/mail_comment.go +++ b/services/mailer/mail_comment.go @@ -8,20 +8,21 @@ import ( "context" "code.gitea.io/gitea/models" + issues_model "code.gitea.io/gitea/models/issues" user_model "code.gitea.io/gitea/models/user" "code.gitea.io/gitea/modules/log" "code.gitea.io/gitea/modules/setting" ) // MailParticipantsComment sends new comment emails to repository watchers and mentioned people. -func MailParticipantsComment(ctx context.Context, c *models.Comment, opType models.ActionType, issue *models.Issue, mentions []*user_model.User) error { +func MailParticipantsComment(ctx context.Context, c *issues_model.Comment, opType models.ActionType, issue *issues_model.Issue, mentions []*user_model.User) error { if setting.MailService == nil { // No mail service configured return nil } content := c.Content - if c.Type == models.CommentTypePullRequestPush { + if c.Type == issues_model.CommentTypePullRequestPush { content = "" } if err := mailIssueCommentToParticipants( @@ -39,7 +40,7 @@ func MailParticipantsComment(ctx context.Context, c *models.Comment, opType mode } // MailMentionsComment sends email to users mentioned in a code comment -func MailMentionsComment(ctx context.Context, pr *models.PullRequest, c *models.Comment, mentions []*user_model.User) (err error) { +func MailMentionsComment(ctx context.Context, pr *issues_model.PullRequest, c *issues_model.Comment, mentions []*user_model.User) (err error) { if setting.MailService == nil { // No mail service configured return nil diff --git a/services/mailer/mail_issue.go b/services/mailer/mail_issue.go index d479dd0d44..5c330f6e00 100644 --- a/services/mailer/mail_issue.go +++ b/services/mailer/mail_issue.go @@ -9,6 +9,7 @@ import ( "fmt" "code.gitea.io/gitea/models" + issues_model "code.gitea.io/gitea/models/issues" repo_model "code.gitea.io/gitea/models/repo" "code.gitea.io/gitea/models/unit" user_model "code.gitea.io/gitea/models/user" @@ -16,17 +17,17 @@ import ( "code.gitea.io/gitea/modules/setting" ) -func fallbackMailSubject(issue *models.Issue) string { +func fallbackMailSubject(issue *issues_model.Issue) string { return fmt.Sprintf("[%s] %s (#%d)", issue.Repo.FullName(), issue.Title, issue.Index) } type mailCommentContext struct { context.Context - Issue *models.Issue + Issue *issues_model.Issue Doer *user_model.User ActionType models.ActionType Content string - Comment *models.Comment + Comment *issues_model.Comment } const ( @@ -57,21 +58,21 @@ func mailIssueCommentToParticipants(ctx *mailCommentContext, mentions []*user_mo unfiltered[0] = ctx.Issue.PosterID // =========== Assignees =========== - ids, err := models.GetAssigneeIDsByIssue(ctx.Issue.ID) + ids, err := issues_model.GetAssigneeIDsByIssue(ctx.Issue.ID) if err != nil { return fmt.Errorf("GetAssigneeIDsByIssue(%d): %v", ctx.Issue.ID, err) } unfiltered = append(unfiltered, ids...) // =========== Participants (i.e. commenters, reviewers) =========== - ids, err = models.GetParticipantsIDsByIssueID(ctx.Issue.ID) + ids, err = issues_model.GetParticipantsIDsByIssueID(ctx.Issue.ID) if err != nil { return fmt.Errorf("GetParticipantsIDsByIssueID(%d): %v", ctx.Issue.ID, err) } unfiltered = append(unfiltered, ids...) // =========== Issue watchers =========== - ids, err = models.GetIssueWatchersIDs(ctx, ctx.Issue.ID, true) + ids, err = issues_model.GetIssueWatchersIDs(ctx, ctx.Issue.ID, true) if err != nil { return fmt.Errorf("GetIssueWatchersIDs(%d): %v", ctx.Issue.ID, err) } @@ -98,7 +99,7 @@ func mailIssueCommentToParticipants(ctx *mailCommentContext, mentions []*user_mo } // Avoid mailing explicit unwatched - ids, err = models.GetIssueWatchersIDs(ctx, ctx.Issue.ID, false) + ids, err = issues_model.GetIssueWatchersIDs(ctx, ctx.Issue.ID, false) if err != nil { return fmt.Errorf("GetIssueWatchersIDs(%d): %v", ctx.Issue.ID, err) } @@ -171,7 +172,7 @@ func mailIssueCommentBatch(ctx *mailCommentContext, users []*user_model.User, vi // MailParticipants sends new issue thread created emails to repository watchers // and mentioned people. -func MailParticipants(issue *models.Issue, doer *user_model.User, opType models.ActionType, mentions []*user_model.User) error { +func MailParticipants(issue *issues_model.Issue, doer *user_model.User, opType models.ActionType, mentions []*user_model.User) error { if setting.MailService == nil { // No mail service configured return nil diff --git a/services/mailer/mail_test.go b/services/mailer/mail_test.go index baf426146a..83955a5896 100644 --- a/services/mailer/mail_test.go +++ b/services/mailer/mail_test.go @@ -15,6 +15,7 @@ import ( "code.gitea.io/gitea/models" "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" @@ -46,7 +47,7 @@ const bodyTpl = ` ` -func prepareMailerTest(t *testing.T) (doer *user_model.User, repo *repo_model.Repository, issue *models.Issue, comment *models.Comment) { +func prepareMailerTest(t *testing.T) (doer *user_model.User, repo *repo_model.Repository, issue *issues_model.Issue, comment *issues_model.Comment) { assert.NoError(t, unittest.PrepareTestDatabase()) mailService := setting.Mailer{ From: "test@gitea.com", @@ -57,9 +58,9 @@ func prepareMailerTest(t *testing.T) (doer *user_model.User, repo *repo_model.Re doer = unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2}).(*user_model.User) repo = unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 1, Owner: doer}).(*repo_model.Repository) - issue = unittest.AssertExistsAndLoadBean(t, &models.Issue{ID: 1, Repo: repo, Poster: doer}).(*models.Issue) + issue = unittest.AssertExistsAndLoadBean(t, &issues_model.Issue{ID: 1, Repo: repo, Poster: doer}).(*issues_model.Issue) assert.NoError(t, issue.LoadRepo(db.DefaultContext)) - comment = unittest.AssertExistsAndLoadBean(t, &models.Comment{ID: 2, Issue: issue}).(*models.Comment) + comment = unittest.AssertExistsAndLoadBean(t, &issues_model.Comment{ID: 2, Issue: issue}).(*issues_model.Comment) return } @@ -162,8 +163,8 @@ func TestTemplateSelection(t *testing.T) { }, recipients, false, "TestTemplateSelection") expect(t, msg, "issue/default/subject", "issue/default/body") - pull := unittest.AssertExistsAndLoadBean(t, &models.Issue{ID: 2, Repo: repo, Poster: doer}).(*models.Issue) - comment = unittest.AssertExistsAndLoadBean(t, &models.Comment{ID: 4, Issue: pull}).(*models.Comment) + pull := unittest.AssertExistsAndLoadBean(t, &issues_model.Issue{ID: 2, Repo: repo, Poster: doer}).(*issues_model.Issue) + comment = unittest.AssertExistsAndLoadBean(t, &issues_model.Comment{ID: 4, Issue: pull}).(*issues_model.Comment) msg = testComposeIssueCommentMessage(t, &mailCommentContext{ Context: context.TODO(), // TODO: use a correct context Issue: pull, Doer: doer, ActionType: models.ActionCommentPull, @@ -183,7 +184,7 @@ func TestTemplateServices(t *testing.T) { doer, _, issue, comment := prepareMailerTest(t) assert.NoError(t, issue.LoadRepo(db.DefaultContext)) - expect := func(t *testing.T, issue *models.Issue, comment *models.Comment, doer *user_model.User, + expect := func(t *testing.T, issue *issues_model.Issue, comment *issues_model.Comment, doer *user_model.User, actionType models.ActionType, fromMention bool, tplSubject, tplBody, expSubject, expBody string, ) { stpl := texttmpl.Must(texttmpl.New("issue/default").Parse(tplSubject)) @@ -268,8 +269,8 @@ func Test_createReference(t *testing.T) { pullIssue.IsPull = true type args struct { - issue *models.Issue - comment *models.Comment + issue *issues_model.Issue + comment *issues_model.Comment actionType models.ActionType } tests := []struct { diff --git a/services/migrations/gitea_uploader.go b/services/migrations/gitea_uploader.go index 408704adef..e71b2ca17a 100644 --- a/services/migrations/gitea_uploader.go +++ b/services/migrations/gitea_uploader.go @@ -45,14 +45,14 @@ type GiteaLocalUploader struct { repoOwner string repoName string repo *repo_model.Repository - labels map[string]*models.Label + labels map[string]*issues_model.Label milestones map[string]int64 - issues map[int64]*models.Issue + issues map[int64]*issues_model.Issue gitRepo *git.Repository prHeadCache map[string]struct{} sameApp bool userMap map[int64]int64 // external user id mapping to user id - prCache map[int64]*models.PullRequest + prCache map[int64]*issues_model.PullRequest gitServiceType structs.GitServiceType } @@ -63,12 +63,12 @@ func NewGiteaLocalUploader(ctx context.Context, doer *user_model.User, repoOwner doer: doer, repoOwner: repoOwner, repoName: repoName, - labels: make(map[string]*models.Label), + labels: make(map[string]*issues_model.Label), milestones: make(map[string]int64), - issues: make(map[int64]*models.Issue), + issues: make(map[int64]*issues_model.Issue), prHeadCache: make(map[string]struct{}), userMap: make(map[int64]int64), - prCache: make(map[int64]*models.PullRequest), + prCache: make(map[int64]*issues_model.PullRequest), } } @@ -76,17 +76,17 @@ func NewGiteaLocalUploader(ctx context.Context, doer *user_model.User, repoOwner func (g *GiteaLocalUploader) MaxBatchInsertSize(tp string) int { switch tp { case "issue": - return db.MaxBatchInsertSize(new(models.Issue)) + return db.MaxBatchInsertSize(new(issues_model.Issue)) case "comment": - return db.MaxBatchInsertSize(new(models.Comment)) + return db.MaxBatchInsertSize(new(issues_model.Comment)) case "milestone": return db.MaxBatchInsertSize(new(issues_model.Milestone)) case "label": - return db.MaxBatchInsertSize(new(models.Label)) + return db.MaxBatchInsertSize(new(issues_model.Label)) case "release": return db.MaxBatchInsertSize(new(models.Release)) case "pullrequest": - return db.MaxBatchInsertSize(new(models.PullRequest)) + return db.MaxBatchInsertSize(new(issues_model.PullRequest)) } return 10 } @@ -216,9 +216,9 @@ func (g *GiteaLocalUploader) CreateMilestones(milestones ...*base.Milestone) err // CreateLabels creates labels func (g *GiteaLocalUploader) CreateLabels(labels ...*base.Label) error { - lbs := make([]*models.Label, 0, len(labels)) + lbs := make([]*issues_model.Label, 0, len(labels)) for _, label := range labels { - lbs = append(lbs, &models.Label{ + lbs = append(lbs, &issues_model.Label{ RepoID: g.repo.ID, Name: label.Name, Description: label.Description, @@ -226,7 +226,7 @@ func (g *GiteaLocalUploader) CreateLabels(labels ...*base.Label) error { }) } - err := models.NewLabels(lbs...) + err := issues_model.NewLabels(lbs...) if err != nil { return err } @@ -339,9 +339,9 @@ func (g *GiteaLocalUploader) SyncTags() error { // CreateIssues creates issues func (g *GiteaLocalUploader) CreateIssues(issues ...*base.Issue) error { - iss := make([]*models.Issue, 0, len(issues)) + iss := make([]*issues_model.Issue, 0, len(issues)) for _, issue := range issues { - var labels []*models.Label + var labels []*issues_model.Label for _, label := range issue.Labels { lb, ok := g.labels[label.Name] if ok { @@ -366,7 +366,7 @@ func (g *GiteaLocalUploader) CreateIssues(issues ...*base.Issue) error { } } - is := models.Issue{ + is := issues_model.Issue{ RepoID: g.repo.ID, Repo: g.repo, Index: issue.Number, @@ -423,9 +423,9 @@ func (g *GiteaLocalUploader) CreateIssues(issues ...*base.Issue) error { // CreateComments creates comments of issues func (g *GiteaLocalUploader) CreateComments(comments ...*base.Comment) error { - cms := make([]*models.Comment, 0, len(comments)) + cms := make([]*issues_model.Comment, 0, len(comments)) for _, comment := range comments { - var issue *models.Issue + var issue *issues_model.Issue issue, ok := g.issues[comment.IssueIndex] if !ok { return fmt.Errorf("comment references non existent IssueIndex %d", comment.IssueIndex) @@ -438,9 +438,9 @@ func (g *GiteaLocalUploader) CreateComments(comments ...*base.Comment) error { comment.Updated = comment.Created } - cm := models.Comment{ + cm := issues_model.Comment{ IssueID: issue.ID, - Type: models.CommentTypeComment, + Type: issues_model.CommentTypeComment, Content: comment.Content, CreatedUnix: timeutil.TimeStamp(comment.Created.Unix()), UpdatedUnix: timeutil.TimeStamp(comment.Updated.Unix()), @@ -473,7 +473,7 @@ func (g *GiteaLocalUploader) CreateComments(comments ...*base.Comment) error { // CreatePullRequests creates pull requests func (g *GiteaLocalUploader) CreatePullRequests(prs ...*base.PullRequest) error { - gprs := make([]*models.PullRequest, 0, len(prs)) + gprs := make([]*issues_model.PullRequest, 0, len(prs)) for _, pr := range prs { gpr, err := g.newPullRequest(pr) if err != nil { @@ -600,8 +600,8 @@ func (g *GiteaLocalUploader) updateGitForPullRequest(pr *base.PullRequest) (head return head, nil } -func (g *GiteaLocalUploader) newPullRequest(pr *base.PullRequest) (*models.PullRequest, error) { - var labels []*models.Label +func (g *GiteaLocalUploader) newPullRequest(pr *base.PullRequest) (*issues_model.PullRequest, error) { + var labels []*issues_model.Label for _, label := range pr.Labels { lb, ok := g.labels[label.Name] if ok { @@ -629,7 +629,7 @@ func (g *GiteaLocalUploader) newPullRequest(pr *base.PullRequest) (*models.PullR pr.Updated = pr.Created } - issue := models.Issue{ + issue := issues_model.Issue{ RepoID: g.repo.ID, Repo: g.repo, Title: pr.Title, @@ -660,7 +660,7 @@ func (g *GiteaLocalUploader) newPullRequest(pr *base.PullRequest) (*models.PullR issue.Reactions = append(issue.Reactions, &res) } - pullRequest := models.PullRequest{ + pullRequest := issues_model.PullRequest{ HeadRepoID: g.repo.ID, HeadBranch: head, BaseRepoID: g.repo.ID, @@ -686,28 +686,28 @@ func (g *GiteaLocalUploader) newPullRequest(pr *base.PullRequest) (*models.PullR return &pullRequest, nil } -func convertReviewState(state string) models.ReviewType { +func convertReviewState(state string) issues_model.ReviewType { switch state { case base.ReviewStatePending: - return models.ReviewTypePending + return issues_model.ReviewTypePending case base.ReviewStateApproved: - return models.ReviewTypeApprove + return issues_model.ReviewTypeApprove case base.ReviewStateChangesRequested: - return models.ReviewTypeReject + return issues_model.ReviewTypeReject case base.ReviewStateCommented: - return models.ReviewTypeComment + return issues_model.ReviewTypeComment case base.ReviewStateRequestReview: - return models.ReviewTypeRequest + return issues_model.ReviewTypeRequest default: - return models.ReviewTypePending + return issues_model.ReviewTypePending } } // CreateReviews create pull request reviews of currently migrated issues func (g *GiteaLocalUploader) CreateReviews(reviews ...*base.Review) error { - cms := make([]*models.Review, 0, len(reviews)) + cms := make([]*issues_model.Review, 0, len(reviews)) for _, review := range reviews { - var issue *models.Issue + var issue *issues_model.Issue issue, ok := g.issues[review.IssueIndex] if !ok { return fmt.Errorf("review references non existent IssueIndex %d", review.IssueIndex) @@ -716,7 +716,7 @@ func (g *GiteaLocalUploader) CreateReviews(reviews ...*base.Review) error { review.CreatedAt = time.Unix(int64(issue.CreatedUnix), 0) } - cm := models.Review{ + cm := issues_model.Review{ Type: convertReviewState(review.State), IssueID: issue.ID, Content: review.Content, @@ -733,7 +733,7 @@ func (g *GiteaLocalUploader) CreateReviews(reviews ...*base.Review) error { pr, ok := g.prCache[issue.ID] if !ok { var err error - pr, err = models.GetPullRequestByIssueIDWithNoAttributes(issue.ID) + pr, err = issues_model.GetPullRequestByIssueIDWithNoAttributes(issue.ID) if err != nil { return err } @@ -767,7 +767,7 @@ func (g *GiteaLocalUploader) CreateReviews(reviews ...*base.Review) error { _ = writer.Close() }(comment) - patch, _ = git.CutDiffAroundLine(reader, int64((&models.Comment{Line: int64(line + comment.Position - 1)}).UnsignedLine()), line < 0, setting.UI.CodeCommentLines) + patch, _ = git.CutDiffAroundLine(reader, int64((&issues_model.Comment{Line: int64(line + comment.Position - 1)}).UnsignedLine()), line < 0, setting.UI.CodeCommentLines) if comment.CreatedAt.IsZero() { comment.CreatedAt = review.CreatedAt @@ -776,8 +776,8 @@ func (g *GiteaLocalUploader) CreateReviews(reviews ...*base.Review) error { comment.UpdatedAt = comment.CreatedAt } - c := models.Comment{ - Type: models.CommentTypeCode, + c := issues_model.Comment{ + Type: issues_model.CommentTypeCode, IssueID: issue.ID, Content: comment.Content, Line: int64(line + comment.Position - 1), @@ -798,7 +798,7 @@ func (g *GiteaLocalUploader) CreateReviews(reviews ...*base.Review) error { cms = append(cms, &cm) } - return models.InsertReviews(cms) + return issues_model.InsertReviews(cms) } // Rollback when migrating failed, this will rollback all the changes. @@ -819,7 +819,7 @@ func (g *GiteaLocalUploader) Finish() error { } // update issue_index - if err := models.RecalculateIssueIndexForRepo(g.repo.ID); err != nil { + if err := issues_model.RecalculateIssueIndexForRepo(g.repo.ID); err != nil { return err } diff --git a/services/migrations/gitea_uploader_test.go b/services/migrations/gitea_uploader_test.go index bd7c6e0657..6ea1c20592 100644 --- a/services/migrations/gitea_uploader_test.go +++ b/services/migrations/gitea_uploader_test.go @@ -81,7 +81,7 @@ func TestGiteaUploadRepo(t *testing.T) { assert.NoError(t, err) assert.Empty(t, milestones) - labels, err := models.GetLabelsByRepoID(ctx, repo.ID, "", db.ListOptions{}) + labels, err := issues_model.GetLabelsByRepoID(ctx, repo.ID, "", db.ListOptions{}) assert.NoError(t, err) assert.Len(t, labels, 12) @@ -105,7 +105,7 @@ func TestGiteaUploadRepo(t *testing.T) { assert.NoError(t, err) assert.Len(t, releases, 1) - issues, err := models.Issues(&models.IssuesOptions{ + issues, err := issues_model.Issues(&issues_model.IssuesOptions{ RepoID: repo.ID, IsPull: util.OptionalBoolFalse, SortType: "oldest", @@ -115,7 +115,7 @@ func TestGiteaUploadRepo(t *testing.T) { assert.NoError(t, issues[0].LoadDiscussComments()) assert.Empty(t, issues[0].Comments) - pulls, _, err := models.PullRequests(repo.ID, &models.PullRequestsOptions{ + pulls, _, err := issues_model.PullRequests(repo.ID, &issues_model.PullRequestsOptions{ SortType: "oldest", }) assert.NoError(t, err) diff --git a/services/pull/check.go b/services/pull/check.go index 94e7ca7161..6621a281fa 100644 --- a/services/pull/check.go +++ b/services/pull/check.go @@ -15,6 +15,7 @@ import ( "code.gitea.io/gitea/models" "code.gitea.io/gitea/models/db" + issues_model "code.gitea.io/gitea/models/issues" access_model "code.gitea.io/gitea/models/perm/access" repo_model "code.gitea.io/gitea/models/repo" "code.gitea.io/gitea/models/unit" @@ -44,9 +45,9 @@ var ( ) // AddToTaskQueue adds itself to pull request test task queue. -func AddToTaskQueue(pr *models.PullRequest) { +func AddToTaskQueue(pr *issues_model.PullRequest) { err := prPatchCheckerQueue.PushFunc(strconv.FormatInt(pr.ID, 10), func() error { - pr.Status = models.PullRequestStatusChecking + pr.Status = issues_model.PullRequestStatusChecking err := pr.UpdateColsIfNotMerged("status") if err != nil { log.Error("AddToTaskQueue.UpdateCols[%d].(add to queue): %v", pr.ID, err) @@ -61,7 +62,7 @@ func AddToTaskQueue(pr *models.PullRequest) { } // CheckPullMergable check if the pull mergable based on all conditions (branch protection, merge options, ...) -func CheckPullMergable(stdCtx context.Context, doer *user_model.User, perm *access_model.Permission, pr *models.PullRequest, manuallMerge, force bool) error { +func CheckPullMergable(stdCtx context.Context, doer *user_model.User, perm *access_model.Permission, pr *issues_model.PullRequest, manuallMerge, force bool) error { return db.WithTx(func(ctx context.Context) error { if pr.HasMerged { return ErrHasMerged @@ -114,7 +115,7 @@ func CheckPullMergable(stdCtx context.Context, doer *user_model.User, perm *acce return err } - if noDeps, err := models.IssueNoDependenciesLeft(ctx, pr.Issue); err != nil { + if noDeps, err := issues_model.IssueNoDependenciesLeft(ctx, pr.Issue); err != nil { return err } else if !noDeps { return ErrDependenciesLeft @@ -125,7 +126,7 @@ func CheckPullMergable(stdCtx context.Context, doer *user_model.User, perm *acce } // isSignedIfRequired check if merge will be signed if required -func isSignedIfRequired(ctx context.Context, pr *models.PullRequest, doer *user_model.User) (bool, error) { +func isSignedIfRequired(ctx context.Context, pr *issues_model.PullRequest, doer *user_model.User) (bool, error) { if err := pr.LoadProtectedBranchCtx(ctx); err != nil { return false, err } @@ -141,10 +142,10 @@ func isSignedIfRequired(ctx context.Context, pr *models.PullRequest, doer *user_ // checkAndUpdateStatus checks if pull request is possible to leaving checking status, // and set to be either conflict or mergeable. -func checkAndUpdateStatus(pr *models.PullRequest) { +func checkAndUpdateStatus(pr *issues_model.PullRequest) { // Status is not changed to conflict means mergeable. - if pr.Status == models.PullRequestStatusChecking { - pr.Status = models.PullRequestStatusMergeable + if pr.Status == issues_model.PullRequestStatusChecking { + pr.Status = issues_model.PullRequestStatusMergeable } // Make sure there is no waiting test to process before leaving the checking status. @@ -162,7 +163,7 @@ func checkAndUpdateStatus(pr *models.PullRequest) { // getMergeCommit checks if a pull request got merged // Returns the git.Commit of the pull request if merged -func getMergeCommit(ctx context.Context, pr *models.PullRequest) (*git.Commit, error) { +func getMergeCommit(ctx context.Context, pr *issues_model.PullRequest) (*git.Commit, error) { if pr.BaseRepo == nil { var err error pr.BaseRepo, err = repo_model.GetRepositoryByID(pr.BaseRepoID) @@ -230,7 +231,7 @@ func getMergeCommit(ctx context.Context, pr *models.PullRequest) (*git.Commit, e // manuallyMerged checks if a pull request got manually merged // When a pull request got manually merged mark the pull request as merged -func manuallyMerged(ctx context.Context, pr *models.PullRequest) bool { +func manuallyMerged(ctx context.Context, pr *issues_model.PullRequest) bool { if err := pr.LoadBaseRepoCtx(ctx); err != nil { log.Error("PullRequest[%d].LoadBaseRepo: %v", pr.ID, err) return false @@ -254,7 +255,7 @@ func manuallyMerged(ctx context.Context, pr *models.PullRequest) bool { if commit != nil { pr.MergedCommitID = commit.ID.String() pr.MergedUnix = timeutil.TimeStamp(commit.Author.When.Unix()) - pr.Status = models.PullRequestStatusManuallyMerged + pr.Status = issues_model.PullRequestStatusManuallyMerged merger, _ := user_model.GetUserByEmail(commit.Author.Email) // When the commit author is unknown set the BaseRepo owner as merger @@ -287,7 +288,7 @@ func manuallyMerged(ctx context.Context, pr *models.PullRequest) bool { // InitializePullRequests checks and tests untested patches of pull requests. func InitializePullRequests(ctx context.Context) { - prs, err := models.GetPullRequestIDsByCheckStatus(models.PullRequestStatusChecking) + prs, err := issues_model.GetPullRequestIDsByCheckStatus(issues_model.PullRequestStatusChecking) if err != nil { log.Error("Find Checking PRs: %v", err) return @@ -323,7 +324,7 @@ func testPR(id int64) { ctx, _, finished := process.GetManager().AddContext(graceful.GetManager().HammerContext(), fmt.Sprintf("Test PR[%d] from patch checking queue", id)) defer finished() - pr, err := models.GetPullRequestByID(ctx, id) + pr, err := issues_model.GetPullRequestByID(ctx, id) if err != nil { log.Error("GetPullRequestByID[%d]: %v", id, err) return @@ -339,7 +340,7 @@ func testPR(id int64) { if err := TestPatch(pr); err != nil { log.Error("testPatch[%d]: %v", pr.ID, err) - pr.Status = models.PullRequestStatusError + pr.Status = issues_model.PullRequestStatusError if err := pr.UpdateCols("status"); err != nil { log.Error("update pr [%d] status to PullRequestStatusError failed: %v", pr.ID, err) } @@ -350,7 +351,7 @@ func testPR(id int64) { // CheckPrsForBaseBranch check all pulls with bseBrannch func CheckPrsForBaseBranch(baseRepo *repo_model.Repository, baseBranchName string) error { - prs, err := models.GetUnmergedPullRequestsByBaseInfo(baseRepo.ID, baseBranchName) + prs, err := issues_model.GetUnmergedPullRequestsByBaseInfo(baseRepo.ID, baseBranchName) if err != nil { return err } diff --git a/services/pull/check_test.go b/services/pull/check_test.go index bc4c45ffad..21fe675bbc 100644 --- a/services/pull/check_test.go +++ b/services/pull/check_test.go @@ -10,7 +10,7 @@ import ( "testing" "time" - "code.gitea.io/gitea/models" + issues_model "code.gitea.io/gitea/models/issues" "code.gitea.io/gitea/models/unittest" "code.gitea.io/gitea/modules/queue" @@ -43,12 +43,12 @@ func TestPullRequest_AddToTaskQueue(t *testing.T) { prPatchCheckerQueue = q.(queue.UniqueQueue) - pr := unittest.AssertExistsAndLoadBean(t, &models.PullRequest{ID: 2}).(*models.PullRequest) + pr := unittest.AssertExistsAndLoadBean(t, &issues_model.PullRequest{ID: 2}).(*issues_model.PullRequest) AddToTaskQueue(pr) assert.Eventually(t, func() bool { - pr = unittest.AssertExistsAndLoadBean(t, &models.PullRequest{ID: 2}).(*models.PullRequest) - return pr.Status == models.PullRequestStatusChecking + pr = unittest.AssertExistsAndLoadBean(t, &issues_model.PullRequest{ID: 2}).(*issues_model.PullRequest) + return pr.Status == issues_model.PullRequestStatusChecking }, 1*time.Second, 100*time.Millisecond) has, err := prPatchCheckerQueue.Has(strconv.FormatInt(pr.ID, 10)) @@ -72,8 +72,8 @@ func TestPullRequest_AddToTaskQueue(t *testing.T) { assert.False(t, has) assert.NoError(t, err) - pr = unittest.AssertExistsAndLoadBean(t, &models.PullRequest{ID: 2}).(*models.PullRequest) - assert.Equal(t, models.PullRequestStatusChecking, pr.Status) + pr = unittest.AssertExistsAndLoadBean(t, &issues_model.PullRequest{ID: 2}).(*issues_model.PullRequest) + assert.Equal(t, issues_model.PullRequestStatusChecking, pr.Status) for _, callback := range queueShutdown { callback() diff --git a/services/pull/commit_status.go b/services/pull/commit_status.go index c0894c6c98..5d846129f6 100644 --- a/services/pull/commit_status.go +++ b/services/pull/commit_status.go @@ -8,9 +8,9 @@ package pull import ( "context" - "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" "code.gitea.io/gitea/modules/git" "code.gitea.io/gitea/modules/structs" @@ -83,7 +83,7 @@ func IsCommitStatusContextSuccess(commitStatuses []*git_model.CommitStatus, requ } // IsPullCommitStatusPass returns if all required status checks PASS -func IsPullCommitStatusPass(ctx context.Context, pr *models.PullRequest) (bool, error) { +func IsPullCommitStatusPass(ctx context.Context, pr *issues_model.PullRequest) (bool, error) { if err := pr.LoadProtectedBranchCtx(ctx); err != nil { return false, errors.Wrap(err, "GetLatestCommitStatus") } @@ -99,7 +99,7 @@ func IsPullCommitStatusPass(ctx context.Context, pr *models.PullRequest) (bool, } // GetPullRequestCommitStatusState returns pull request merged commit status state -func GetPullRequestCommitStatusState(ctx context.Context, pr *models.PullRequest) (structs.CommitStatusState, error) { +func GetPullRequestCommitStatusState(ctx context.Context, pr *issues_model.PullRequest) (structs.CommitStatusState, error) { // Ensure HeadRepo is loaded if err := pr.LoadHeadRepoCtx(ctx); err != nil { return "", errors.Wrap(err, "LoadHeadRepo") @@ -112,15 +112,15 @@ func GetPullRequestCommitStatusState(ctx context.Context, pr *models.PullRequest } defer closer.Close() - if pr.Flow == models.PullRequestFlowGithub && !headGitRepo.IsBranchExist(pr.HeadBranch) { + if pr.Flow == issues_model.PullRequestFlowGithub && !headGitRepo.IsBranchExist(pr.HeadBranch) { return "", errors.New("Head branch does not exist, can not merge") } - if pr.Flow == models.PullRequestFlowAGit && !git.IsReferenceExist(ctx, headGitRepo.Path, pr.GetGitRefName()) { + if pr.Flow == issues_model.PullRequestFlowAGit && !git.IsReferenceExist(ctx, headGitRepo.Path, pr.GetGitRefName()) { return "", errors.New("Head branch does not exist, can not merge") } var sha string - if pr.Flow == models.PullRequestFlowGithub { + if pr.Flow == issues_model.PullRequestFlowGithub { sha, err = headGitRepo.GetBranchCommitID(pr.HeadBranch) } else { sha, err = headGitRepo.GetRefCommitID(pr.GetGitRefName()) diff --git a/services/pull/edits.go b/services/pull/edits.go index 11932d9ab8..2938f2b108 100644 --- a/services/pull/edits.go +++ b/services/pull/edits.go @@ -9,7 +9,7 @@ import ( "context" "errors" - "code.gitea.io/gitea/models" + issues_model "code.gitea.io/gitea/models/issues" access_model "code.gitea.io/gitea/models/perm/access" unit_model "code.gitea.io/gitea/models/unit" user_model "code.gitea.io/gitea/models/user" @@ -18,7 +18,7 @@ import ( var ErrUserHasNoPermissionForAction = errors.New("user not allowed to do this action") // SetAllowEdits allow edits from maintainers to PRs -func SetAllowEdits(ctx context.Context, doer *user_model.User, pr *models.PullRequest, allow bool) error { +func SetAllowEdits(ctx context.Context, doer *user_model.User, pr *issues_model.PullRequest, allow bool) error { if doer == nil || !pr.Issue.IsPoster(doer.ID) { return ErrUserHasNoPermissionForAction } @@ -37,5 +37,5 @@ func SetAllowEdits(ctx context.Context, doer *user_model.User, pr *models.PullRe } pr.AllowMaintainerEdit = allow - return models.UpdateAllowEdits(ctx, pr) + return issues_model.UpdateAllowEdits(ctx, pr) } diff --git a/services/pull/lfs.go b/services/pull/lfs.go index 490a904584..8cca0a91b7 100644 --- a/services/pull/lfs.go +++ b/services/pull/lfs.go @@ -12,15 +12,15 @@ import ( "strconv" "sync" - "code.gitea.io/gitea/models" git_model "code.gitea.io/gitea/models/git" + issues_model "code.gitea.io/gitea/models/issues" "code.gitea.io/gitea/modules/git/pipeline" "code.gitea.io/gitea/modules/lfs" "code.gitea.io/gitea/modules/log" ) // LFSPush pushes lfs objects referred to in new commits in the head repository from the base repository -func LFSPush(ctx context.Context, tmpBasePath, mergeHeadSHA, mergeBaseSHA string, pr *models.PullRequest) error { +func LFSPush(ctx context.Context, tmpBasePath, mergeHeadSHA, mergeBaseSHA string, pr *issues_model.PullRequest) error { // Now we have to implement git lfs push // git rev-list --objects --filter=blob:limit=1k HEAD --not base // pass blob shas in to git cat-file --batch-check (possibly unnecessary) @@ -68,7 +68,7 @@ func LFSPush(ctx context.Context, tmpBasePath, mergeHeadSHA, mergeBaseSHA string return nil } -func createLFSMetaObjectsFromCatFileBatch(catFileBatchReader *io.PipeReader, wg *sync.WaitGroup, pr *models.PullRequest) { +func createLFSMetaObjectsFromCatFileBatch(catFileBatchReader *io.PipeReader, wg *sync.WaitGroup, pr *issues_model.PullRequest) { defer wg.Done() defer catFileBatchReader.Close() diff --git a/services/pull/merge.go b/services/pull/merge.go index eef1d17b64..aff800a1b6 100644 --- a/services/pull/merge.go +++ b/services/pull/merge.go @@ -20,6 +20,7 @@ import ( "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" access_model "code.gitea.io/gitea/models/perm/access" pull_model "code.gitea.io/gitea/models/pull" repo_model "code.gitea.io/gitea/models/repo" @@ -38,7 +39,7 @@ import ( ) // GetDefaultMergeMessage returns default message used when merging pull request -func GetDefaultMergeMessage(baseGitRepo *git.Repository, pr *models.PullRequest, mergeStyle repo_model.MergeStyle) (string, error) { +func GetDefaultMergeMessage(baseGitRepo *git.Repository, pr *issues_model.PullRequest, mergeStyle repo_model.MergeStyle) (string, error) { if err := pr.LoadHeadRepo(); err != nil { return "", err } @@ -131,7 +132,7 @@ func GetDefaultMergeMessage(baseGitRepo *git.Repository, pr *models.PullRequest, // Merge merges pull request to base repository. // Caller should check PR is ready to be merged (review and status checks) -func Merge(ctx context.Context, pr *models.PullRequest, doer *user_model.User, baseGitRepo *git.Repository, mergeStyle repo_model.MergeStyle, expectedHeadCommitID, message string) error { +func Merge(ctx context.Context, pr *issues_model.PullRequest, doer *user_model.User, baseGitRepo *git.Repository, mergeStyle repo_model.MergeStyle, expectedHeadCommitID, message string) error { if err := pr.LoadHeadRepo(); err != nil { log.Error("LoadHeadRepo: %v", err) return fmt.Errorf("LoadHeadRepo: %v", err) @@ -213,7 +214,7 @@ func Merge(ctx context.Context, pr *models.PullRequest, doer *user_model.User, b if close != ref.Issue.IsClosed { if err = issue_service.ChangeStatus(ref.Issue, doer, close); err != nil { // Allow ErrDependenciesLeft - if !models.IsErrDependenciesLeft(err) { + if !issues_model.IsErrDependenciesLeft(err) { return err } } @@ -223,7 +224,7 @@ func Merge(ctx context.Context, pr *models.PullRequest, doer *user_model.User, b } // rawMerge perform the merge operation without changing any pull information in database -func rawMerge(ctx context.Context, pr *models.PullRequest, doer *user_model.User, mergeStyle repo_model.MergeStyle, expectedHeadCommitID, message string) (string, error) { +func rawMerge(ctx context.Context, pr *issues_model.PullRequest, doer *user_model.User, mergeStyle repo_model.MergeStyle, expectedHeadCommitID, message string) (string, error) { // Clone base repo. tmpBasePath, err := createTemporaryRepo(ctx, pr) if err != nil { @@ -635,7 +636,7 @@ func rawMerge(ctx context.Context, pr *models.PullRequest, doer *user_model.User return mergeCommitID, nil } -func commitAndSignNoAuthor(ctx context.Context, pr *models.PullRequest, message, signArg, tmpBasePath string, env []string) error { +func commitAndSignNoAuthor(ctx context.Context, pr *issues_model.PullRequest, message, signArg, tmpBasePath string, env []string) error { var outbuf, errbuf strings.Builder if signArg == "" { if err := git.NewCommand(ctx, "commit", "-m", message). @@ -663,7 +664,7 @@ func commitAndSignNoAuthor(ctx context.Context, pr *models.PullRequest, message, return nil } -func runMergeCommand(pr *models.PullRequest, mergeStyle repo_model.MergeStyle, cmd *git.Command, tmpBasePath string) error { +func runMergeCommand(pr *issues_model.PullRequest, mergeStyle repo_model.MergeStyle, cmd *git.Command, tmpBasePath string) error { var outbuf, errbuf strings.Builder if err := cmd.Run(&git.RunOpts{ Dir: tmpBasePath, @@ -747,7 +748,7 @@ func getDiffTree(ctx context.Context, repoPath, baseBranch, headBranch string) ( } // IsUserAllowedToMerge check if user is allowed to merge PR with given permissions and branch protections -func IsUserAllowedToMerge(ctx context.Context, pr *models.PullRequest, p access_model.Permission, user *user_model.User) (bool, error) { +func IsUserAllowedToMerge(ctx context.Context, pr *issues_model.PullRequest, p access_model.Permission, user *user_model.User) (bool, error) { if user == nil { return false, nil } @@ -765,7 +766,7 @@ func IsUserAllowedToMerge(ctx context.Context, pr *models.PullRequest, p access_ } // CheckPullBranchProtections checks whether the PR is ready to be merged (reviews and status checks) -func CheckPullBranchProtections(ctx context.Context, pr *models.PullRequest, skipProtectedFilesCheck bool) (err error) { +func CheckPullBranchProtections(ctx context.Context, pr *issues_model.PullRequest, skipProtectedFilesCheck bool) (err error) { if err = pr.LoadBaseRepoCtx(ctx); err != nil { return fmt.Errorf("LoadBaseRepo: %v", err) } @@ -787,23 +788,23 @@ func CheckPullBranchProtections(ctx context.Context, pr *models.PullRequest, ski } } - if !models.HasEnoughApprovals(ctx, pr.ProtectedBranch, pr) { + if !issues_model.HasEnoughApprovals(ctx, pr.ProtectedBranch, pr) { return models.ErrDisallowedToMerge{ Reason: "Does not have enough approvals", } } - if models.MergeBlockedByRejectedReview(ctx, pr.ProtectedBranch, pr) { + if issues_model.MergeBlockedByRejectedReview(ctx, pr.ProtectedBranch, pr) { return models.ErrDisallowedToMerge{ Reason: "There are requested changes", } } - if models.MergeBlockedByOfficialReviewRequests(ctx, pr.ProtectedBranch, pr) { + if issues_model.MergeBlockedByOfficialReviewRequests(ctx, pr.ProtectedBranch, pr) { return models.ErrDisallowedToMerge{ Reason: "There are official review requests", } } - if models.MergeBlockedByOutdatedBranch(pr.ProtectedBranch, pr) { + if issues_model.MergeBlockedByOutdatedBranch(pr.ProtectedBranch, pr) { return models.ErrDisallowedToMerge{ Reason: "The head branch is behind the base branch", } @@ -823,7 +824,7 @@ func CheckPullBranchProtections(ctx context.Context, pr *models.PullRequest, ski } // MergedManually mark pr as merged manually -func MergedManually(pr *models.PullRequest, doer *user_model.User, baseGitRepo *git.Repository, commitID string) error { +func MergedManually(pr *issues_model.PullRequest, doer *user_model.User, baseGitRepo *git.Repository, commitID string) error { pullWorkingPool.CheckIn(fmt.Sprint(pr.ID)) defer pullWorkingPool.CheckOut(fmt.Sprint(pr.ID)) @@ -862,7 +863,7 @@ func MergedManually(pr *models.PullRequest, doer *user_model.User, baseGitRepo * pr.MergedCommitID = commitID pr.MergedUnix = timeutil.TimeStamp(commit.Author.When.Unix()) - pr.Status = models.PullRequestStatusManuallyMerged + pr.Status = issues_model.PullRequestStatusManuallyMerged pr.Merger = doer pr.MergerID = doer.ID diff --git a/services/pull/patch.go b/services/pull/patch.go index 6e2889b060..c7a69501c3 100644 --- a/services/pull/patch.go +++ b/services/pull/patch.go @@ -15,6 +15,7 @@ import ( "strings" "code.gitea.io/gitea/models" + issues_model "code.gitea.io/gitea/models/issues" "code.gitea.io/gitea/models/unit" "code.gitea.io/gitea/modules/git" "code.gitea.io/gitea/modules/graceful" @@ -27,7 +28,7 @@ import ( ) // DownloadDiffOrPatch will write the patch for the pr to the writer -func DownloadDiffOrPatch(ctx context.Context, pr *models.PullRequest, w io.Writer, patch, binary bool) error { +func DownloadDiffOrPatch(ctx context.Context, pr *issues_model.PullRequest, w io.Writer, patch, binary bool) error { if err := pr.LoadBaseRepoCtx(ctx); err != nil { log.Error("Unable to load base repository ID %d for pr #%d [%d]", pr.BaseRepoID, pr.Index, pr.ID) return err @@ -54,7 +55,7 @@ var patchErrorSuffices = []string{ } // TestPatch will test whether a simple patch will apply -func TestPatch(pr *models.PullRequest) error { +func TestPatch(pr *issues_model.PullRequest) error { ctx, _, finished := process.GetManager().AddContext(graceful.GetManager().HammerContext(), fmt.Sprintf("TestPatch: Repo[%d]#%d", pr.BaseRepoID, pr.Index)) defer finished() @@ -88,7 +89,7 @@ func TestPatch(pr *models.PullRequest) error { pr.MergeBase = strings.TrimSpace(pr.MergeBase) // 2. Check for conflicts - if conflicts, err := checkConflicts(ctx, pr, gitRepo, tmpBasePath); err != nil || conflicts || pr.Status == models.PullRequestStatusEmpty { + if conflicts, err := checkConflicts(ctx, pr, gitRepo, tmpBasePath); err != nil || conflicts || pr.Status == issues_model.PullRequestStatusEmpty { return err } @@ -101,7 +102,7 @@ func TestPatch(pr *models.PullRequest) error { log.Trace("Found %d protected files changed", len(pr.ChangedProtectedFiles)) } - pr.Status = models.PullRequestStatusMergeable + pr.Status = issues_model.PullRequestStatusMergeable return nil } @@ -270,7 +271,7 @@ func AttemptThreeWayMerge(ctx context.Context, gitPath string, gitRepo *git.Repo return conflict, conflictedFiles, nil } -func checkConflicts(ctx context.Context, pr *models.PullRequest, gitRepo *git.Repository, tmpBasePath string) (bool, error) { +func checkConflicts(ctx context.Context, pr *issues_model.PullRequest, gitRepo *git.Repository, tmpBasePath string) (bool, error) { // 1. checkConflicts resets the conflict status - therefore - reset the conflict status pr.ConflictedFiles = nil @@ -295,7 +296,7 @@ func checkConflicts(ctx context.Context, pr *models.PullRequest, gitRepo *git.Re } if treeHash == baseTree.ID.String() { log.Debug("PullRequest[%d]: Patch is empty - ignoring", pr.ID) - pr.Status = models.PullRequestStatusEmpty + pr.Status = issues_model.PullRequestStatusEmpty } return false, nil @@ -329,7 +330,7 @@ func checkConflicts(ctx context.Context, pr *models.PullRequest, gitRepo *git.Re // 3b. if the size of that patch is 0 - there can be no conflicts! if stat.Size() == 0 { log.Debug("PullRequest[%d]: Patch is empty - ignoring", pr.ID) - pr.Status = models.PullRequestStatusEmpty + pr.Status = issues_model.PullRequestStatusEmpty return false, nil } @@ -449,7 +450,7 @@ func checkConflicts(ctx context.Context, pr *models.PullRequest, gitRepo *git.Re // Note: `"err" could be non-nil` is due that if enable 3-way merge, it doesn't return any error on found conflicts. if len(pr.ConflictedFiles) > 0 { if conflict { - pr.Status = models.PullRequestStatusConflict + pr.Status = issues_model.PullRequestStatusConflict log.Trace("Found %d files conflicted: %v", len(pr.ConflictedFiles), pr.ConflictedFiles) return true, nil @@ -516,8 +517,8 @@ func CheckUnprotectedFiles(repo *git.Repository, oldCommitID, newCommitID string } // checkPullFilesProtection check if pr changed protected files and save results -func checkPullFilesProtection(pr *models.PullRequest, gitRepo *git.Repository) error { - if pr.Status == models.PullRequestStatusEmpty { +func checkPullFilesProtection(pr *issues_model.PullRequest, gitRepo *git.Repository) error { + if pr.Status == issues_model.PullRequestStatusEmpty { pr.ChangedProtectedFiles = nil return nil } diff --git a/services/pull/pull.go b/services/pull/pull.go index 736520fda2..103fdc340d 100644 --- a/services/pull/pull.go +++ b/services/pull/pull.go @@ -17,6 +17,7 @@ import ( "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" user_model "code.gitea.io/gitea/models/user" "code.gitea.io/gitea/modules/git" @@ -35,7 +36,7 @@ import ( var pullWorkingPool = sync.NewExclusivePool() // NewPullRequest creates new pull request with labels for repository. -func NewPullRequest(ctx context.Context, repo *repo_model.Repository, pull *models.Issue, labelIDs []int64, uuids []string, pr *models.PullRequest, assigneeIDs []int64) error { +func NewPullRequest(ctx context.Context, repo *repo_model.Repository, pull *issues_model.Issue, labelIDs []int64, uuids []string, pr *issues_model.PullRequest, assigneeIDs []int64) error { if err := TestPatch(pr); err != nil { return err } @@ -47,7 +48,7 @@ func NewPullRequest(ctx context.Context, repo *repo_model.Repository, pull *mode pr.CommitsAhead = divergence.Ahead pr.CommitsBehind = divergence.Behind - if err := models.NewPullRequest(ctx, repo, pull, labelIDs, uuids, pr); err != nil { + if err := issues_model.NewPullRequest(ctx, repo, pull, labelIDs, uuids, pr); err != nil { return err } @@ -66,7 +67,7 @@ func NewPullRequest(ctx context.Context, repo *repo_model.Repository, pull *mode prCtx, _, finished := process.GetManager().AddContext(graceful.GetManager().HammerContext(), fmt.Sprintf("NewPullRequest: %s:%d", repo.FullName(), pr.Index)) defer finished() - if pr.Flow == models.PullRequestFlowGithub { + if pr.Flow == issues_model.PullRequestFlowGithub { err = PushToBaseRepo(prCtx, pr) } else { err = UpdateRef(prCtx, pr) @@ -75,7 +76,7 @@ func NewPullRequest(ctx context.Context, repo *repo_model.Repository, pull *mode return err } - mentions, err := models.FindAndUpdateIssueMentions(ctx, pull, pull.Poster, pull.Content) + mentions, err := issues_model.FindAndUpdateIssueMentions(ctx, pull, pull.Poster, pull.Content) if err != nil { return err } @@ -102,7 +103,7 @@ func NewPullRequest(ctx context.Context, repo *repo_model.Repository, pull *mode } if len(compareInfo.Commits) > 0 { - data := models.PushActionContent{IsForcePush: false} + data := issues_model.PushActionContent{IsForcePush: false} data.CommitIDs = make([]string, 0, len(compareInfo.Commits)) for i := len(compareInfo.Commits) - 1; i >= 0; i-- { data.CommitIDs = append(data.CommitIDs, compareInfo.Commits[i].ID.String()) @@ -113,8 +114,8 @@ func NewPullRequest(ctx context.Context, repo *repo_model.Repository, pull *mode return err } - ops := &models.CreateCommentOptions{ - Type: models.CommentTypePullRequestPush, + ops := &issues_model.CreateCommentOptions{ + Type: issues_model.CommentTypePullRequestPush, Doer: pull.Poster, Repo: repo, Issue: pr.Issue, @@ -122,14 +123,14 @@ func NewPullRequest(ctx context.Context, repo *repo_model.Repository, pull *mode Content: string(dataJSON), } - _, _ = models.CreateComment(ops) + _, _ = issues_model.CreateComment(ops) } return nil } // ChangeTargetBranch changes the target branch of this pull request, as the given user. -func ChangeTargetBranch(ctx context.Context, pr *models.PullRequest, doer *user_model.User, targetBranch string) (err error) { +func ChangeTargetBranch(ctx context.Context, pr *issues_model.PullRequest, doer *user_model.User, targetBranch string) (err error) { pullWorkingPool.CheckIn(fmt.Sprint(pr.ID)) defer pullWorkingPool.CheckOut(fmt.Sprint(pr.ID)) @@ -139,7 +140,7 @@ func ChangeTargetBranch(ctx context.Context, pr *models.PullRequest, doer *user_ } if pr.Issue.IsClosed { - return models.ErrIssueIsClosed{ + return issues_model.ErrIssueIsClosed{ ID: pr.Issue.ID, RepoID: pr.Issue.RepoID, Index: pr.Issue.Index, @@ -170,9 +171,9 @@ func ChangeTargetBranch(ctx context.Context, pr *models.PullRequest, doer *user_ } // Check if pull request for the new target branch already exists - existingPr, err := models.GetUnmergedPullRequest(pr.HeadRepoID, pr.BaseRepoID, pr.HeadBranch, targetBranch, models.PullRequestFlowGithub) + existingPr, err := issues_model.GetUnmergedPullRequest(pr.HeadRepoID, pr.BaseRepoID, pr.HeadBranch, targetBranch, issues_model.PullRequestFlowGithub) if existingPr != nil { - return models.ErrPullRequestAlreadyExists{ + return issues_model.ErrPullRequestAlreadyExists{ ID: existingPr.ID, IssueID: existingPr.Index, HeadRepoID: existingPr.HeadRepoID, @@ -181,7 +182,7 @@ func ChangeTargetBranch(ctx context.Context, pr *models.PullRequest, doer *user_ BaseBranch: existingPr.BaseBranch, } } - if err != nil && !models.IsErrPullRequestNotExist(err) { + if err != nil && !issues_model.IsErrPullRequestNotExist(err) { return err } @@ -196,8 +197,8 @@ func ChangeTargetBranch(ctx context.Context, pr *models.PullRequest, doer *user_ // Update target branch, PR diff and status // This is the same as checkAndUpdateStatus in check service, but also updates base_branch - if pr.Status == models.PullRequestStatusChecking { - pr.Status = models.PullRequestStatusMergeable + if pr.Status == issues_model.PullRequestStatusChecking { + pr.Status = issues_model.PullRequestStatusMergeable } // Update Commit Divergence @@ -213,22 +214,22 @@ func ChangeTargetBranch(ctx context.Context, pr *models.PullRequest, doer *user_ } // Create comment - options := &models.CreateCommentOptions{ - Type: models.CommentTypeChangeTargetBranch, + options := &issues_model.CreateCommentOptions{ + Type: issues_model.CommentTypeChangeTargetBranch, Doer: doer, Repo: pr.Issue.Repo, Issue: pr.Issue, OldRef: oldBranch, NewRef: targetBranch, } - if _, err = models.CreateComment(options); err != nil { + if _, err = issues_model.CreateComment(options); err != nil { return fmt.Errorf("CreateChangeTargetBranchComment: %v", err) } return nil } -func checkForInvalidation(ctx context.Context, requests models.PullRequestList, repoID int64, doer *user_model.User, branch string) error { +func checkForInvalidation(ctx context.Context, requests issues_model.PullRequestList, repoID int64, doer *user_model.User, branch string) error { repo, err := repo_model.GetRepositoryByID(repoID) if err != nil { return fmt.Errorf("GetRepositoryByID: %v", err) @@ -257,14 +258,14 @@ func AddTestPullRequestTask(doer *user_model.User, repoID int64, branch string, // If you don't let it run all the way then you will lose data // TODO: graceful: AddTestPullRequestTask needs to become a queue! - prs, err := models.GetUnmergedPullRequestsByHeadInfo(repoID, branch) + prs, err := issues_model.GetUnmergedPullRequestsByHeadInfo(repoID, branch) if err != nil { log.Error("Find pull requests [head_repo_id: %d, head_branch: %s]: %v", repoID, branch, err) return } if isSync { - requests := models.PullRequestList(prs) + requests := issues_model.PullRequestList(prs) if err = requests.LoadAttributes(); err != nil { log.Error("PullRequestList.LoadAttributes: %v", err) } @@ -280,11 +281,11 @@ func AddTestPullRequestTask(doer *user_model.User, repoID int64, branch string, } if changed { // Mark old reviews as stale if diff to mergebase has changed - if err := models.MarkReviewsAsStale(pr.IssueID); err != nil { + if err := issues_model.MarkReviewsAsStale(pr.IssueID); err != nil { log.Error("MarkReviewsAsStale: %v", err) } } - if err := models.MarkReviewsAsNotStale(pr.IssueID, newCommitID); err != nil { + if err := issues_model.MarkReviewsAsNotStale(pr.IssueID, newCommitID); err != nil { log.Error("MarkReviewsAsNotStale: %v", err) } divergence, err := GetDiverging(ctx, pr) @@ -306,7 +307,7 @@ func AddTestPullRequestTask(doer *user_model.User, repoID int64, branch string, for _, pr := range prs { log.Trace("Updating PR[%d]: composing new test task", pr.ID) - if pr.Flow == models.PullRequestFlowGithub { + if pr.Flow == issues_model.PullRequestFlowGithub { if err := PushToBaseRepo(ctx, pr); err != nil { log.Error("PushToBaseRepo: %v", err) continue @@ -316,14 +317,14 @@ func AddTestPullRequestTask(doer *user_model.User, repoID int64, branch string, } AddToTaskQueue(pr) - comment, err := models.CreatePushPullComment(ctx, doer, pr, oldCommitID, newCommitID) + comment, err := issues_model.CreatePushPullComment(ctx, doer, pr, oldCommitID, newCommitID) if err == nil && comment != nil { notification.NotifyPullRequestPushCommits(doer, pr, comment) } } log.Trace("AddTestPullRequestTask [base_repo_id: %d, base_branch: %s]: finding pull requests", repoID, branch) - prs, err = models.GetUnmergedPullRequestsByBaseInfo(repoID, branch) + prs, err = issues_model.GetUnmergedPullRequestsByBaseInfo(repoID, branch) if err != nil { log.Error("Find pull requests [base_repo_id: %d, base_branch: %s]: %v", repoID, branch, err) return @@ -349,7 +350,7 @@ func AddTestPullRequestTask(doer *user_model.User, repoID int64, branch string, // checkIfPRContentChanged checks if diff to target branch has changed by push // A commit can be considered to leave the PR untouched if the patch/diff with its merge base is unchanged -func checkIfPRContentChanged(ctx context.Context, pr *models.PullRequest, oldCommitID, newCommitID string) (hasChanged bool, err error) { +func checkIfPRContentChanged(ctx context.Context, pr *issues_model.PullRequest, oldCommitID, newCommitID string) (hasChanged bool, err error) { if err = pr.LoadHeadRepoCtx(ctx); err != nil { return false, fmt.Errorf("LoadHeadRepo: %v", err) } else if pr.HeadRepo == nil { @@ -421,11 +422,11 @@ func checkIfPRContentChanged(ctx context.Context, pr *models.PullRequest, oldCom // PushToBaseRepo pushes commits from branches of head repository to // corresponding branches of base repository. // FIXME: Only push branches that are actually updates? -func PushToBaseRepo(ctx context.Context, pr *models.PullRequest) (err error) { +func PushToBaseRepo(ctx context.Context, pr *issues_model.PullRequest) (err error) { return pushToBaseRepoHelper(ctx, pr, "") } -func pushToBaseRepoHelper(ctx context.Context, pr *models.PullRequest, prefixHeadBranch string) (err error) { +func pushToBaseRepoHelper(ctx context.Context, pr *issues_model.PullRequest, prefixHeadBranch string) (err error) { log.Trace("PushToBaseRepo[%d]: pushing commits to base repo '%s'", pr.BaseRepoID, pr.GetGitRefName()) if err := pr.LoadHeadRepoCtx(ctx); err != nil { @@ -481,7 +482,7 @@ func pushToBaseRepoHelper(ctx context.Context, pr *models.PullRequest, prefixHea } // UpdateRef update refs/pull/id/head directly for agit flow pull request -func UpdateRef(ctx context.Context, pr *models.PullRequest) (err error) { +func UpdateRef(ctx context.Context, pr *issues_model.PullRequest) (err error) { log.Trace("UpdateRef[%d]: upgate pull request ref in base repo '%s'", pr.ID, pr.GetGitRefName()) if err := pr.LoadBaseRepoCtx(ctx); err != nil { log.Error("Unable to load base repository for PR[%d] Error: %v", pr.ID, err) @@ -514,24 +515,24 @@ func (errs errlist) Error() string { // CloseBranchPulls close all the pull requests who's head branch is the branch func CloseBranchPulls(doer *user_model.User, repoID int64, branch string) error { - prs, err := models.GetUnmergedPullRequestsByHeadInfo(repoID, branch) + prs, err := issues_model.GetUnmergedPullRequestsByHeadInfo(repoID, branch) if err != nil { return err } - prs2, err := models.GetUnmergedPullRequestsByBaseInfo(repoID, branch) + prs2, err := issues_model.GetUnmergedPullRequestsByBaseInfo(repoID, branch) if err != nil { return err } prs = append(prs, prs2...) - if err := models.PullRequestList(prs).LoadAttributes(); err != nil { + if err := issues_model.PullRequestList(prs).LoadAttributes(); err != nil { return err } var errs errlist for _, pr := range prs { - if err = issue_service.ChangeStatus(pr.Issue, doer, true); err != nil && !models.IsErrPullWasClosed(err) && !models.IsErrDependenciesLeft(err) { + if err = issue_service.ChangeStatus(pr.Issue, doer, true); err != nil && !issues_model.IsErrPullWasClosed(err) && !issues_model.IsErrDependenciesLeft(err) { errs = append(errs, err) } } @@ -550,12 +551,12 @@ func CloseRepoBranchesPulls(ctx context.Context, doer *user_model.User, repo *re var errs errlist for _, branch := range branches { - prs, err := models.GetUnmergedPullRequestsByHeadInfo(repo.ID, branch.Name) + prs, err := issues_model.GetUnmergedPullRequestsByHeadInfo(repo.ID, branch.Name) if err != nil { return err } - if err = models.PullRequestList(prs).LoadAttributes(); err != nil { + if err = issues_model.PullRequestList(prs).LoadAttributes(); err != nil { return err } @@ -565,7 +566,7 @@ func CloseRepoBranchesPulls(ctx context.Context, doer *user_model.User, repo *re if pr.BaseRepoID == repo.ID { continue } - if err = issue_service.ChangeStatus(pr.Issue, doer, true); err != nil && !models.IsErrPullWasClosed(err) { + if err = issue_service.ChangeStatus(pr.Issue, doer, true); err != nil && !issues_model.IsErrPullWasClosed(err) { errs = append(errs, err) } } @@ -580,7 +581,7 @@ func CloseRepoBranchesPulls(ctx context.Context, doer *user_model.User, repo *re var commitMessageTrailersPattern = regexp.MustCompile(`(?:^|\n\n)(?:[\w-]+[ \t]*:[^\n]+\n*(?:[ \t]+[^\n]+\n*)*)+$`) // GetSquashMergeCommitMessages returns the commit messages between head and merge base (if there is one) -func GetSquashMergeCommitMessages(ctx context.Context, pr *models.PullRequest) string { +func GetSquashMergeCommitMessages(ctx context.Context, pr *issues_model.PullRequest) string { if err := pr.LoadIssue(); err != nil { log.Error("Cannot load issue %d for PR id %d: Error: %v", pr.IssueID, pr.ID, err) return "" @@ -608,7 +609,7 @@ func GetSquashMergeCommitMessages(ctx context.Context, pr *models.PullRequest) s defer closer.Close() var headCommit *git.Commit - if pr.Flow == models.PullRequestFlowGithub { + if pr.Flow == issues_model.PullRequestFlowGithub { headCommit, err = gitRepo.GetBranchCommit(pr.HeadBranch) } else { pr.HeadCommitID, err = gitRepo.GetRefCommitID(pr.GetGitRefName()) @@ -736,13 +737,13 @@ func GetSquashMergeCommitMessages(ctx context.Context, pr *models.PullRequest) s } // GetIssuesLastCommitStatus returns a map of issue ID to the most recent commit's latest status -func GetIssuesLastCommitStatus(ctx context.Context, issues models.IssueList) (map[int64]*git_model.CommitStatus, error) { +func GetIssuesLastCommitStatus(ctx context.Context, issues issues_model.IssueList) (map[int64]*git_model.CommitStatus, error) { _, lastStatus, err := GetIssuesAllCommitStatus(ctx, issues) return lastStatus, err } // GetIssuesAllCommitStatus returns a map of issue ID to a list of all statuses for the most recent commit as well as a map of issue ID to only the commit's latest status -func GetIssuesAllCommitStatus(ctx context.Context, issues models.IssueList) (map[int64][]*git_model.CommitStatus, map[int64]*git_model.CommitStatus, error) { +func GetIssuesAllCommitStatus(ctx context.Context, issues issues_model.IssueList) (map[int64][]*git_model.CommitStatus, map[int64]*git_model.CommitStatus, error) { if err := issues.LoadPullRequests(); err != nil { return nil, nil, err } @@ -788,7 +789,7 @@ func GetIssuesAllCommitStatus(ctx context.Context, issues models.IssueList) (map } // getAllCommitStatus get pr's commit statuses. -func getAllCommitStatus(gitRepo *git.Repository, pr *models.PullRequest) (statuses []*git_model.CommitStatus, lastStatus *git_model.CommitStatus, err error) { +func getAllCommitStatus(gitRepo *git.Repository, pr *issues_model.PullRequest) (statuses []*git_model.CommitStatus, lastStatus *git_model.CommitStatus, err error) { sha, shaErr := gitRepo.GetRefCommitID(pr.GetGitRefName()) if shaErr != nil { return nil, nil, shaErr @@ -800,7 +801,7 @@ func getAllCommitStatus(gitRepo *git.Repository, pr *models.PullRequest) (status } // IsHeadEqualWithBranch returns if the commits of branchName are available in pull request head -func IsHeadEqualWithBranch(ctx context.Context, pr *models.PullRequest, branchName string) (bool, error) { +func IsHeadEqualWithBranch(ctx context.Context, pr *issues_model.PullRequest, branchName string) (bool, error) { var err error if err = pr.LoadBaseRepoCtx(ctx); err != nil { return false, err @@ -833,7 +834,7 @@ func IsHeadEqualWithBranch(ctx context.Context, pr *models.PullRequest, branchNa } var headCommit *git.Commit - if pr.Flow == models.PullRequestFlowGithub { + if pr.Flow == issues_model.PullRequestFlowGithub { headCommit, err = headGitRepo.GetBranchCommit(pr.HeadBranch) if err != nil { return false, err diff --git a/services/pull/pull_test.go b/services/pull/pull_test.go index 09bae97780..9160c43460 100644 --- a/services/pull/pull_test.go +++ b/services/pull/pull_test.go @@ -8,7 +8,7 @@ package pull import ( "testing" - "code.gitea.io/gitea/models" + issues_model "code.gitea.io/gitea/models/issues" repo_model "code.gitea.io/gitea/models/repo" "code.gitea.io/gitea/models/unit" "code.gitea.io/gitea/models/unittest" @@ -38,7 +38,7 @@ func TestPullRequest_CommitMessageTrailersPattern(t *testing.T) { func TestPullRequest_GetDefaultMergeMessage_InternalTracker(t *testing.T) { assert.NoError(t, unittest.PrepareTestDatabase()) - pr := unittest.AssertExistsAndLoadBean(t, &models.PullRequest{ID: 2}).(*models.PullRequest) + pr := unittest.AssertExistsAndLoadBean(t, &issues_model.PullRequest{ID: 2}).(*issues_model.PullRequest) assert.NoError(t, pr.LoadBaseRepo()) gitRepo, err := git.OpenRepository(git.DefaultContext, pr.BaseRepo.RepoPath()) @@ -68,7 +68,7 @@ func TestPullRequest_GetDefaultMergeMessage_ExternalTracker(t *testing.T) { baseRepo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 1}).(*repo_model.Repository) baseRepo.Units = []*repo_model.RepoUnit{&externalTracker} - pr := unittest.AssertExistsAndLoadBean(t, &models.PullRequest{ID: 2, BaseRepo: baseRepo}).(*models.PullRequest) + pr := unittest.AssertExistsAndLoadBean(t, &issues_model.PullRequest{ID: 2, BaseRepo: baseRepo}).(*issues_model.PullRequest) assert.NoError(t, pr.LoadBaseRepo()) gitRepo, err := git.OpenRepository(git.DefaultContext, pr.BaseRepo.RepoPath()) diff --git a/services/pull/review.go b/services/pull/review.go index eac7279f9b..9cb58fa3a1 100644 --- a/services/pull/review.go +++ b/services/pull/review.go @@ -12,8 +12,8 @@ import ( "regexp" "strings" - "code.gitea.io/gitea/models" "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" @@ -23,7 +23,7 @@ import ( ) // CreateCodeComment creates a comment on the code line -func CreateCodeComment(ctx context.Context, doer *user_model.User, gitRepo *git.Repository, issue *models.Issue, line int64, content, treePath string, isReview bool, replyReviewID int64, latestCommitID string) (*models.Comment, error) { +func CreateCodeComment(ctx context.Context, doer *user_model.User, gitRepo *git.Repository, issue *issues_model.Issue, line int64, content, treePath string, isReview bool, replyReviewID int64, latestCommitID string) (*issues_model.Comment, error) { var ( existsReview bool err error @@ -37,7 +37,7 @@ func CreateCodeComment(ctx context.Context, doer *user_model.User, gitRepo *git. if !isReview && replyReviewID != 0 { // It's not part of a review; maybe a reply to a review comment or a single comment. // Check if there are reviews for that line already; if there are, this is a reply - if existsReview, err = models.ReviewExists(issue, treePath, line); err != nil { + if existsReview, err = issues_model.ReviewExists(issue, treePath, line); err != nil { return nil, err } } @@ -61,7 +61,7 @@ func CreateCodeComment(ctx context.Context, doer *user_model.User, gitRepo *git. return nil, err } - mentions, err := models.FindAndUpdateIssueMentions(ctx, issue, doer, comment.Content) + mentions, err := issues_model.FindAndUpdateIssueMentions(ctx, issue, doer, comment.Content) if err != nil { return nil, err } @@ -71,14 +71,14 @@ func CreateCodeComment(ctx context.Context, doer *user_model.User, gitRepo *git. return comment, nil } - review, err := models.GetCurrentReview(ctx, doer, issue) + review, err := issues_model.GetCurrentReview(ctx, doer, issue) if err != nil { - if !models.IsErrReviewNotExist(err) { + if !issues_model.IsErrReviewNotExist(err) { return nil, err } - if review, err = models.CreateReview(ctx, models.CreateReviewOptions{ - Type: models.ReviewTypePending, + if review, err = issues_model.CreateReview(ctx, issues_model.CreateReviewOptions{ + Type: issues_model.ReviewTypePending, Reviewer: doer, Issue: issue, Official: false, @@ -103,7 +103,7 @@ func CreateCodeComment(ctx context.Context, doer *user_model.User, gitRepo *git. if !isReview && !existsReview { // Submit the review we've just created so the comment shows up in the issue view - if _, _, err = SubmitReview(ctx, doer, gitRepo, issue, models.ReviewTypeComment, "", latestCommitID, nil); err != nil { + if _, _, err = SubmitReview(ctx, doer, gitRepo, issue, issues_model.ReviewTypeComment, "", latestCommitID, nil); err != nil { return nil, err } } @@ -116,7 +116,7 @@ func CreateCodeComment(ctx context.Context, doer *user_model.User, gitRepo *git. var notEnoughLines = regexp.MustCompile(`exit status 128 - fatal: file .* has only \d+ lines?`) // createCodeComment creates a plain code comment at the specified line / path -func createCodeComment(ctx context.Context, doer *user_model.User, repo *repo_model.Repository, issue *models.Issue, content, treePath string, line, reviewID int64) (*models.Comment, error) { +func createCodeComment(ctx context.Context, doer *user_model.User, repo *repo_model.Repository, issue *issues_model.Issue, content, treePath string, line, reviewID int64) (*issues_model.Comment, error) { var commitID, patch string if err := issue.LoadPullRequest(); err != nil { return nil, fmt.Errorf("GetPullRequestByIssueID: %v", err) @@ -135,11 +135,11 @@ func createCodeComment(ctx context.Context, doer *user_model.User, repo *repo_mo head := pr.GetGitRefName() if line > 0 { if reviewID != 0 { - first, err := models.FindComments(ctx, &models.FindCommentsOptions{ + first, err := issues_model.FindComments(ctx, &issues_model.FindCommentsOptions{ ReviewID: reviewID, Line: line, TreePath: treePath, - Type: models.CommentTypeCode, + Type: issues_model.CommentTypeCode, ListOptions: db.ListOptions{ PageSize: 1, Page: 1, @@ -149,13 +149,13 @@ func createCodeComment(ctx context.Context, doer *user_model.User, repo *repo_mo commitID = first[0].CommitSHA invalidated = first[0].Invalidated patch = first[0].Patch - } else if err != nil && !models.IsErrCommentNotExist(err) { + } else if err != nil && !issues_model.IsErrCommentNotExist(err) { return nil, fmt.Errorf("Find first comment for %d line %d path %s. Error: %v", reviewID, line, treePath, err) } else { - review, err := models.GetReviewByID(ctx, reviewID) + review, err := issues_model.GetReviewByID(ctx, reviewID) if err == nil && len(review.CommitID) > 0 { head = review.CommitID - } else if err != nil && !models.IsErrReviewNotExist(err) { + } else if err != nil && !issues_model.IsErrReviewNotExist(err) { return nil, fmt.Errorf("GetReviewByID %d. Error: %v", reviewID, err) } } @@ -196,14 +196,14 @@ func createCodeComment(ctx context.Context, doer *user_model.User, repo *repo_mo _ = writer.Close() }() - patch, err = git.CutDiffAroundLine(reader, int64((&models.Comment{Line: line}).UnsignedLine()), line < 0, setting.UI.CodeCommentLines) + patch, err = git.CutDiffAroundLine(reader, int64((&issues_model.Comment{Line: line}).UnsignedLine()), line < 0, setting.UI.CodeCommentLines) if err != nil { log.Error("Error whilst generating patch: %v", err) return nil, err } } - return models.CreateComment(&models.CreateCommentOptions{ - Type: models.CommentTypeCode, + return issues_model.CreateComment(&issues_model.CreateCommentOptions{ + Type: issues_model.CommentTypeCode, Doer: doer, Repo: repo, Issue: issue, @@ -218,14 +218,14 @@ func createCodeComment(ctx context.Context, doer *user_model.User, repo *repo_mo } // SubmitReview creates a review out of the existing pending review or creates a new one if no pending review exist -func SubmitReview(ctx context.Context, doer *user_model.User, gitRepo *git.Repository, issue *models.Issue, reviewType models.ReviewType, content, commitID string, attachmentUUIDs []string) (*models.Review, *models.Comment, error) { +func SubmitReview(ctx context.Context, doer *user_model.User, gitRepo *git.Repository, issue *issues_model.Issue, reviewType issues_model.ReviewType, content, commitID string, attachmentUUIDs []string) (*issues_model.Review, *issues_model.Comment, error) { pr, err := issue.GetPullRequest() if err != nil { return nil, nil, err } var stale bool - if reviewType != models.ReviewTypeApprove && reviewType != models.ReviewTypeReject { + if reviewType != issues_model.ReviewTypeApprove && reviewType != issues_model.ReviewTypeReject { stale = false } else { headCommitID, err := gitRepo.GetRefCommitID(pr.GetGitRefName()) @@ -243,12 +243,12 @@ func SubmitReview(ctx context.Context, doer *user_model.User, gitRepo *git.Repos } } - review, comm, err := models.SubmitReview(doer, issue, reviewType, content, commitID, stale, attachmentUUIDs) + review, comm, err := issues_model.SubmitReview(doer, issue, reviewType, content, commitID, stale, attachmentUUIDs) if err != nil { return nil, nil, err } - mentions, err := models.FindAndUpdateIssueMentions(ctx, issue, doer, comm.Content) + mentions, err := issues_model.FindAndUpdateIssueMentions(ctx, issue, doer, comm.Content) if err != nil { return nil, nil, err } @@ -258,7 +258,7 @@ func SubmitReview(ctx context.Context, doer *user_model.User, gitRepo *git.Repos for _, lines := range review.CodeComments { for _, comments := range lines { for _, codeComment := range comments { - mentions, err := models.FindAndUpdateIssueMentions(ctx, issue, doer, codeComment.Content) + mentions, err := issues_model.FindAndUpdateIssueMentions(ctx, issue, doer, codeComment.Content) if err != nil { return nil, nil, err } @@ -271,17 +271,17 @@ func SubmitReview(ctx context.Context, doer *user_model.User, gitRepo *git.Repos } // DismissReview dismissing stale review by repo admin -func DismissReview(ctx context.Context, reviewID int64, message string, doer *user_model.User, isDismiss bool) (comment *models.Comment, err error) { - review, err := models.GetReviewByID(ctx, reviewID) +func DismissReview(ctx context.Context, reviewID int64, message string, doer *user_model.User, isDismiss bool) (comment *issues_model.Comment, err error) { + review, err := issues_model.GetReviewByID(ctx, reviewID) if err != nil { return } - if review.Type != models.ReviewTypeApprove && review.Type != models.ReviewTypeReject { + if review.Type != issues_model.ReviewTypeApprove && review.Type != issues_model.ReviewTypeReject { return nil, fmt.Errorf("not need to dismiss this review because it's type is not Approve or change request") } - if err = models.DismissReview(review, isDismiss); err != nil { + if err = issues_model.DismissReview(review, isDismiss); err != nil { return } @@ -296,14 +296,14 @@ func DismissReview(ctx context.Context, reviewID int64, message string, doer *us if err = review.Issue.LoadPullRequest(); err != nil { return } - if err = review.Issue.LoadAttributes(); err != nil { + if err = review.Issue.LoadAttributes(ctx); err != nil { return } - comment, err = models.CreateComment(&models.CreateCommentOptions{ + comment, err = issues_model.CreateComment(&issues_model.CreateCommentOptions{ Doer: doer, Content: message, - Type: models.CommentTypeDismissReview, + Type: issues_model.CommentTypeDismissReview, ReviewID: review.ID, Issue: review.Issue, Repo: review.Issue.Repo, diff --git a/services/pull/temp_repo.go b/services/pull/temp_repo.go index 6b01809d49..c1456ef0a9 100644 --- a/services/pull/temp_repo.go +++ b/services/pull/temp_repo.go @@ -13,6 +13,7 @@ import ( "strings" "code.gitea.io/gitea/models" + issues_model "code.gitea.io/gitea/models/issues" repo_model "code.gitea.io/gitea/models/repo" "code.gitea.io/gitea/modules/git" "code.gitea.io/gitea/modules/log" @@ -21,7 +22,7 @@ import ( // createTemporaryRepo creates a temporary repo with "base" for pr.BaseBranch and "tracking" for pr.HeadBranch // it also create a second base branch called "original_base" -func createTemporaryRepo(ctx context.Context, pr *models.PullRequest) (string, error) { +func createTemporaryRepo(ctx context.Context, pr *issues_model.PullRequest) (string, error) { if err := pr.LoadHeadRepoCtx(ctx); err != nil { log.Error("LoadHeadRepo: %v", err) return "", fmt.Errorf("LoadHeadRepo: %v", err) @@ -164,7 +165,7 @@ func createTemporaryRepo(ctx context.Context, pr *models.PullRequest) (string, e trackingBranch := "tracking" // Fetch head branch var headBranch string - if pr.Flow == models.PullRequestFlowGithub { + if pr.Flow == issues_model.PullRequestFlowGithub { headBranch = git.BranchPrefix + pr.HeadBranch } else if len(pr.HeadCommitID) == 40 { // for not created pull request headBranch = pr.HeadCommitID diff --git a/services/pull/update.go b/services/pull/update.go index 0ab8ffcd7d..e5e26462e5 100644 --- a/services/pull/update.go +++ b/services/pull/update.go @@ -9,6 +9,7 @@ import ( "fmt" "code.gitea.io/gitea/models" + issues_model "code.gitea.io/gitea/models/issues" access_model "code.gitea.io/gitea/models/perm/access" repo_model "code.gitea.io/gitea/models/repo" "code.gitea.io/gitea/models/unit" @@ -19,9 +20,9 @@ import ( ) // Update updates pull request with base branch. -func Update(ctx context.Context, pull *models.PullRequest, doer *user_model.User, message string, rebase bool) error { +func Update(ctx context.Context, pull *issues_model.PullRequest, doer *user_model.User, message string, rebase bool) error { var ( - pr *models.PullRequest + pr *issues_model.PullRequest style repo_model.MergeStyle ) @@ -33,7 +34,7 @@ func Update(ctx context.Context, pull *models.PullRequest, doer *user_model.User style = repo_model.MergeStyleRebaseUpdate } else { // use merge functions but switch repo's and branch's - pr = &models.PullRequest{ + pr = &issues_model.PullRequest{ HeadRepoID: pull.BaseRepoID, BaseRepoID: pull.HeadRepoID, HeadBranch: pull.BaseBranch, @@ -42,7 +43,7 @@ func Update(ctx context.Context, pull *models.PullRequest, doer *user_model.User style = repo_model.MergeStyleMerge } - if pull.Flow == models.PullRequestFlowAGit { + if pull.Flow == issues_model.PullRequestFlowAGit { // TODO: Not support update agit flow pull request's head branch return fmt.Errorf("Not support update agit flow pull request's head branch") } @@ -76,8 +77,8 @@ func Update(ctx context.Context, pull *models.PullRequest, doer *user_model.User } // IsUserAllowedToUpdate check if user is allowed to update PR with given permissions and branch protections -func IsUserAllowedToUpdate(ctx context.Context, pull *models.PullRequest, user *user_model.User) (mergeAllowed, rebaseAllowed bool, err error) { - if pull.Flow == models.PullRequestFlowAGit { +func IsUserAllowedToUpdate(ctx context.Context, pull *issues_model.PullRequest, user *user_model.User) (mergeAllowed, rebaseAllowed bool, err error) { + if pull.Flow == issues_model.PullRequestFlowAGit { return false, false, nil } @@ -89,7 +90,7 @@ func IsUserAllowedToUpdate(ctx context.Context, pull *models.PullRequest, user * return false, false, err } - pr := &models.PullRequest{ + pr := &issues_model.PullRequest{ HeadRepoID: pull.BaseRepoID, BaseRepoID: pull.HeadRepoID, HeadBranch: pull.BaseBranch, @@ -139,7 +140,7 @@ func IsUserAllowedToUpdate(ctx context.Context, pull *models.PullRequest, user * } // GetDiverging determines how many commits a PR is ahead or behind the PR base branch -func GetDiverging(ctx context.Context, pr *models.PullRequest) (*git.DivergeObject, error) { +func GetDiverging(ctx context.Context, pr *issues_model.PullRequest) (*git.DivergeObject, error) { log.Trace("GetDiverging[%d]: compare commits", pr.ID) if err := pr.LoadBaseRepoCtx(ctx); err != nil { return nil, err diff --git a/services/repository/repository.go b/services/repository/repository.go index 6848eda101..4bde6879a6 100644 --- a/services/repository/repository.go +++ b/services/repository/repository.go @@ -11,6 +11,7 @@ import ( "code.gitea.io/gitea/models" admin_model "code.gitea.io/gitea/models/admin" "code.gitea.io/gitea/models/db" + issues_model "code.gitea.io/gitea/models/issues" "code.gitea.io/gitea/models/organization" packages_model "code.gitea.io/gitea/models/packages" repo_model "code.gitea.io/gitea/models/repo" @@ -105,7 +106,7 @@ func UpdateRepository(repo *repo_model.Repository, visibilityChanged bool) (err // LinkedRepository returns the linked repo if any func LinkedRepository(a *repo_model.Attachment) (*repo_model.Repository, unit.Type, error) { if a.IssueID != 0 { - iss, err := models.GetIssueByID(a.IssueID) + iss, err := issues_model.GetIssueByID(db.DefaultContext, a.IssueID) if err != nil { return nil, unit.TypeIssues, err } diff --git a/services/repository/template.go b/services/repository/template.go index 6a1bfaff5b..d7e8145811 100644 --- a/services/repository/template.go +++ b/services/repository/template.go @@ -9,6 +9,7 @@ import ( "code.gitea.io/gitea/models" "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/log" @@ -18,14 +19,14 @@ import ( // GenerateIssueLabels generates issue labels from a template repository func GenerateIssueLabels(ctx context.Context, templateRepo, generateRepo *repo_model.Repository) error { - templateLabels, err := models.GetLabelsByRepoID(ctx, templateRepo.ID, "", db.ListOptions{}) + templateLabels, err := issues_model.GetLabelsByRepoID(ctx, templateRepo.ID, "", db.ListOptions{}) if err != nil { return err } - newLabels := make([]*models.Label, 0, len(templateLabels)) + newLabels := make([]*issues_model.Label, 0, len(templateLabels)) for _, templateLabel := range templateLabels { - newLabels = append(newLabels, &models.Label{ + newLabels = append(newLabels, &issues_model.Label{ RepoID: generateRepo.ID, Name: templateLabel.Name, Description: templateLabel.Description, -- cgit v1.2.3