aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorOtto Richter (fnetX)2022-03-01 01:20:15 +0100
committerGitHub2022-03-01 01:20:15 +0100
commit062fd4c217cc7302f56acf043d6214a9db46ee2f (patch)
tree4aaa51baaee1d7ddfab00a88a2d22f5911af1312
parent6859b6919800cbf2958dbfbe76fca42f4dcbb194 (diff)
[API] Allow removing issues (#18879)
Add new feature to delete issues and pulls via API Co-authored-by: fnetx <git@fralix.ovh> Co-authored-by: Lunny Xiao <xiaolunwen@gmail.com> Co-authored-by: Gusted <williamzijl7@hotmail.com> Co-authored-by: 6543 <6543@obermui.de>
-rw-r--r--models/issue.go114
-rw-r--r--models/issue_comment.go4
-rw-r--r--models/issue_test.go52
-rw-r--r--modules/nosql/manager_leveldb.go1
-rw-r--r--modules/notification/base/notifier.go1
-rw-r--r--modules/notification/base/null.go4
-rw-r--r--modules/notification/notification.go7
-rw-r--r--routers/api/v1/api.go3
-rw-r--r--routers/api/v1/repo/issue.go46
-rw-r--r--services/issue/issue.go29
-rw-r--r--templates/swagger/v1_json.tmpl42
11 files changed, 299 insertions, 4 deletions
diff --git a/models/issue.go b/models/issue.go
index 91d4df32d..625374faa 100644
--- a/models/issue.go
+++ b/models/issue.go
@@ -13,6 +13,7 @@ import (
"strconv"
"strings"
+ admin_model "code.gitea.io/gitea/models/admin"
"code.gitea.io/gitea/models/db"
"code.gitea.io/gitea/models/issues"
"code.gitea.io/gitea/models/perm"
@@ -24,6 +25,7 @@ import (
"code.gitea.io/gitea/modules/log"
"code.gitea.io/gitea/modules/references"
"code.gitea.io/gitea/modules/setting"
+ "code.gitea.io/gitea/modules/storage"
api "code.gitea.io/gitea/modules/structs"
"code.gitea.io/gitea/modules/timeutil"
"code.gitea.io/gitea/modules/util"
@@ -1990,6 +1992,118 @@ func UpdateIssueDeadline(issue *Issue, deadlineUnix timeutil.TimeStamp, doer *us
return committer.Commit()
}
+// DeleteIssue deletes the issue
+func DeleteIssue(issue *Issue) error {
+ ctx, committer, err := db.TxContext()
+ if err != nil {
+ return err
+ }
+ defer committer.Close()
+
+ if err := deleteIssue(ctx, issue); err != nil {
+ return err
+ }
+
+ return committer.Commit()
+}
+
+func deleteInIssue(e db.Engine, issueID int64, beans ...interface{}) error {
+ 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
+ var comments []int64
+ if err := e.Table(new(Comment)).In("issue_id", issue.ID).Cols("id").Find(&comments); err != nil {
+ return err
+ }
+ for i := range comments {
+ if _, err := e.Where("comment_id = ?", comments[i]).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(e, issue.ID,
+ &issues.ContentHistory{},
+ &Comment{},
+ &IssueLabel{},
+ &IssueDependency{},
+ &IssueAssignees{},
+ &IssueUser{},
+ &Reaction{},
+ &IssueWatch{},
+ &Stopwatch{},
+ &TrackedTime{},
+ &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"`
diff --git a/models/issue_comment.go b/models/issue_comment.go
index 31bd041ca..0af45e80e 100644
--- a/models/issue_comment.go
+++ b/models/issue_comment.go
@@ -1152,9 +1152,7 @@ func DeleteComment(comment *Comment) error {
}
func deleteComment(e db.Engine, comment *Comment) error {
- if _, err := e.Delete(&Comment{
- ID: comment.ID,
- }); err != nil {
+ if _, err := e.ID(comment.ID).NoAutoCondition().Delete(comment); err != nil {
return err
}
diff --git a/models/issue_test.go b/models/issue_test.go
index 9344d385a..7cc0aa61b 100644
--- a/models/issue_test.go
+++ b/models/issue_test.go
@@ -397,6 +397,58 @@ func TestIssue_InsertIssue(t *testing.T) {
assert.NoError(t, err)
}
+func TestIssue_DeleteIssue(t *testing.T) {
+ assert.NoError(t, unittest.PrepareTestDatabase())
+
+ issueIDs, err := GetIssueIDsByRepoID(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(1)
+ assert.NoError(t, err)
+ assert.EqualValues(t, 4, len(issueIDs))
+
+ // check attachment removal
+ attachments, err := repo_model.GetAttachmentsByIssueID(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(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(issue1)
+ assert.NoError(t, err)
+ assert.False(t, left)
+ err = DeleteIssue(&Issue{ID: 2})
+ assert.NoError(t, err)
+ left, err = IssueNoDependenciesLeft(issue1)
+ assert.NoError(t, err)
+ assert.True(t, left)
+}
+
func TestIssue_ResolveMentions(t *testing.T) {
assert.NoError(t, unittest.PrepareTestDatabase())
diff --git a/modules/nosql/manager_leveldb.go b/modules/nosql/manager_leveldb.go
index 97f917af7..de4ef14d7 100644
--- a/modules/nosql/manager_leveldb.go
+++ b/modules/nosql/manager_leveldb.go
@@ -11,6 +11,7 @@ import (
"strings"
"code.gitea.io/gitea/modules/log"
+
"github.com/syndtr/goleveldb/leveldb"
"github.com/syndtr/goleveldb/leveldb/errors"
"github.com/syndtr/goleveldb/leveldb/opt"
diff --git a/modules/notification/base/notifier.go b/modules/notification/base/notifier.go
index 7f5caa3bc..817474116 100644
--- a/modules/notification/base/notifier.go
+++ b/modules/notification/base/notifier.go
@@ -22,6 +22,7 @@ type Notifier interface {
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)
diff --git a/modules/notification/base/null.go b/modules/notification/base/null.go
index bd52b843a..2bfcaafda 100644
--- a/modules/notification/base/null.go
+++ b/modules/notification/base/null.go
@@ -33,6 +33,10 @@ func (*NullNotifier) NotifyNewIssue(issue *models.Issue, mentions []*user_model.
func (*NullNotifier) NotifyIssueChangeStatus(doer *user_model.User, issue *models.Issue, actionComment *models.Comment, isClosed bool) {
}
+// NotifyDeleteIssue notify when some issue deleted
+func (*NullNotifier) NotifyDeleteIssue(doer *user_model.User, issue *models.Issue) {
+}
+
// NotifyNewPullRequest places a place holder function
func (*NullNotifier) NotifyNewPullRequest(pr *models.PullRequest, mentions []*user_model.User) {
}
diff --git a/modules/notification/notification.go b/modules/notification/notification.go
index e8d5d07b3..a31e3810e 100644
--- a/modules/notification/notification.go
+++ b/modules/notification/notification.go
@@ -60,6 +60,13 @@ func NotifyIssueChangeStatus(doer *user_model.User, issue *models.Issue, actionC
}
}
+// NotifyDeleteIssue notify when some issue deleted
+func NotifyDeleteIssue(doer *user_model.User, issue *models.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) {
for _, notifier := range notifiers {
diff --git a/routers/api/v1/api.go b/routers/api/v1/api.go
index 6d8ab8ce9..d4891daef 100644
--- a/routers/api/v1/api.go
+++ b/routers/api/v1/api.go
@@ -835,7 +835,8 @@ func Routes(sessioner func(http.Handler) http.Handler) *web.Route {
})
m.Group("/{index}", func() {
m.Combo("").Get(repo.GetIssue).
- Patch(reqToken(), bind(api.EditIssueOption{}), repo.EditIssue)
+ Patch(reqToken(), bind(api.EditIssueOption{}), repo.EditIssue).
+ Delete(reqToken(), reqAdmin(), repo.DeleteIssue)
m.Group("/comments", func() {
m.Combo("").Get(repo.ListIssueComments).
Post(reqToken(), mustNotBeArchived, bind(api.CreateIssueCommentOption{}), repo.CreateIssueComment)
diff --git a/routers/api/v1/repo/issue.go b/routers/api/v1/repo/issue.go
index e2afa7249..9e550c4c4 100644
--- a/routers/api/v1/repo/issue.go
+++ b/routers/api/v1/repo/issue.go
@@ -834,6 +834,52 @@ func EditIssue(ctx *context.APIContext) {
ctx.JSON(http.StatusCreated, convert.ToAPIIssue(issue))
}
+func DeleteIssue(ctx *context.APIContext) {
+ // swagger:operation DELETE /repos/{owner}/{repo}/issues/{index} issue issueDelete
+ // ---
+ // summary: Delete an issue
+ // parameters:
+ // - name: owner
+ // in: path
+ // description: owner of the repo
+ // type: string
+ // required: true
+ // - name: repo
+ // in: path
+ // description: name of the repo
+ // type: string
+ // required: true
+ // - name: index
+ // in: path
+ // description: index of issue to delete
+ // type: integer
+ // format: int64
+ // required: true
+ // responses:
+ // "204":
+ // "$ref": "#/responses/empty"
+ // "403":
+ // "$ref": "#/responses/forbidden"
+ // "404":
+ // "$ref": "#/responses/notFound"
+ issue, err := models.GetIssueByIndex(ctx.Repo.Repository.ID, ctx.ParamsInt64(":index"))
+ if err != nil {
+ if models.IsErrIssueNotExist(err) {
+ ctx.NotFound(err)
+ } else {
+ ctx.Error(http.StatusInternalServerError, "GetIssueByID", err)
+ }
+ return
+ }
+
+ if err = issue_service.DeleteIssue(ctx.User, ctx.Repo.GitRepo, issue); err != nil {
+ ctx.Error(http.StatusInternalServerError, "DeleteIssueByID", err)
+ return
+ }
+
+ ctx.Status(http.StatusNoContent)
+}
+
// UpdateIssueDeadline updates an issue deadline
func UpdateIssueDeadline(ctx *context.APIContext) {
// swagger:operation POST /repos/{owner}/{repo}/issues/{index}/deadline issue issueEditIssueDeadline
diff --git a/services/issue/issue.go b/services/issue/issue.go
index 8b6262c57..6e5e4bfd3 100644
--- a/services/issue/issue.go
+++ b/services/issue/issue.go
@@ -5,6 +5,8 @@
package issue
import (
+ "fmt"
+
"code.gitea.io/gitea/models"
"code.gitea.io/gitea/models/db"
repo_model "code.gitea.io/gitea/models/repo"
@@ -125,6 +127,33 @@ func UpdateAssignees(issue *models.Issue, oneAssignee string, multipleAssignees
return
}
+// DeleteIssue deletes an issue
+func DeleteIssue(doer *user_model.User, gitRepo *git.Repository, issue *models.Issue) error {
+ // load issue before deleting it
+ if err := issue.LoadAttributes(); err != nil {
+ return err
+ }
+ if err := issue.LoadPullRequest(); err != nil {
+ return err
+ }
+
+ // delete entries in database
+ if err := models.DeleteIssue(issue); err != nil {
+ return err
+ }
+
+ // delete pull request related git data
+ if issue.IsPull {
+ if err := gitRepo.RemoveReference(fmt.Sprintf("%s%d", git.PullPrefix, issue.PullRequest.Index)); err != nil {
+ return err
+ }
+ }
+
+ notification.NotifyDeleteIssue(doer, issue)
+
+ return nil
+}
+
// 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) {
diff --git a/templates/swagger/v1_json.tmpl b/templates/swagger/v1_json.tmpl
index 0b0b83ebb..69abe2402 100644
--- a/templates/swagger/v1_json.tmpl
+++ b/templates/swagger/v1_json.tmpl
@@ -4966,6 +4966,48 @@
}
}
},
+ "delete": {
+ "tags": [
+ "issue"
+ ],
+ "summary": "Delete an issue",
+ "operationId": "issueDelete",
+ "parameters": [
+ {
+ "type": "string",
+ "description": "owner of the repo",
+ "name": "owner",
+ "in": "path",
+ "required": true
+ },
+ {
+ "type": "string",
+ "description": "name of the repo",
+ "name": "repo",
+ "in": "path",
+ "required": true
+ },
+ {
+ "type": "integer",
+ "format": "int64",
+ "description": "index of issue to delete",
+ "name": "index",
+ "in": "path",
+ "required": true
+ }
+ ],
+ "responses": {
+ "204": {
+ "$ref": "#/responses/empty"
+ },
+ "403": {
+ "$ref": "#/responses/forbidden"
+ },
+ "404": {
+ "$ref": "#/responses/notFound"
+ }
+ }
+ },
"patch": {
"consumes": [
"application/json"