Skip to content

Commit 732d5e5

Browse files
fnetXlunnyGusted6543
committed
CB/bp: [API] Allow removing issues (go-gitea#18879)
Add new feature to delete issues and pulls via API Co-authored-by: fnetx <[email protected]> Co-authored-by: Lunny Xiao <[email protected]> Co-authored-by: Gusted <[email protected]> Co-authored-by: 6543 <[email protected]>
1 parent 607f740 commit 732d5e5

File tree

10 files changed

+298
-4
lines changed

10 files changed

+298
-4
lines changed

models/issue.go

Lines changed: 114 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import (
1313
"strconv"
1414
"strings"
1515

16+
admin_model "code.gitea.io/gitea/models/admin"
1617
"code.gitea.io/gitea/models/db"
1718
"code.gitea.io/gitea/models/issues"
1819
"code.gitea.io/gitea/models/perm"
@@ -23,6 +24,7 @@ import (
2324
"code.gitea.io/gitea/modules/git"
2425
"code.gitea.io/gitea/modules/log"
2526
"code.gitea.io/gitea/modules/references"
27+
"code.gitea.io/gitea/modules/storage"
2628
api "code.gitea.io/gitea/modules/structs"
2729
"code.gitea.io/gitea/modules/timeutil"
2830
"code.gitea.io/gitea/modules/util"
@@ -1991,6 +1993,118 @@ func UpdateIssueDeadline(issue *Issue, deadlineUnix timeutil.TimeStamp, doer *us
19911993
return committer.Commit()
19921994
}
19931995

1996+
// DeleteIssue deletes the issue
1997+
func DeleteIssue(issue *Issue) error {
1998+
ctx, committer, err := db.TxContext()
1999+
if err != nil {
2000+
return err
2001+
}
2002+
defer committer.Close()
2003+
2004+
if err := deleteIssue(ctx, issue); err != nil {
2005+
return err
2006+
}
2007+
2008+
return committer.Commit()
2009+
}
2010+
2011+
func deleteInIssue(e db.Engine, issueID int64, beans ...interface{}) error {
2012+
for _, bean := range beans {
2013+
if _, err := e.In("issue_id", issueID).Delete(bean); err != nil {
2014+
return err
2015+
}
2016+
}
2017+
return nil
2018+
}
2019+
2020+
func deleteIssue(ctx context.Context, issue *Issue) error {
2021+
e := db.GetEngine(ctx)
2022+
if _, err := e.ID(issue.ID).NoAutoCondition().Delete(issue); err != nil {
2023+
return err
2024+
}
2025+
2026+
if issue.IsPull {
2027+
if _, err := e.ID(issue.RepoID).Decr("num_pulls").Update(new(repo_model.Repository)); err != nil {
2028+
return err
2029+
}
2030+
if issue.IsClosed {
2031+
if _, err := e.ID(issue.RepoID).Decr("num_closed_pulls").Update(new(repo_model.Repository)); err != nil {
2032+
return err
2033+
}
2034+
}
2035+
} else {
2036+
if _, err := e.ID(issue.RepoID).Decr("num_issues").Update(new(repo_model.Repository)); err != nil {
2037+
return err
2038+
}
2039+
if issue.IsClosed {
2040+
if _, err := e.ID(issue.RepoID).Decr("num_closed_issues").Update(new(repo_model.Repository)); err != nil {
2041+
return err
2042+
}
2043+
}
2044+
}
2045+
2046+
// delete actions assigned to this issue
2047+
var comments []int64
2048+
if err := e.Table(new(Comment)).In("issue_id", issue.ID).Cols("id").Find(&comments); err != nil {
2049+
return err
2050+
}
2051+
for i := range comments {
2052+
if _, err := e.Where("comment_id = ?", comments[i]).Delete(&Action{}); err != nil {
2053+
return err
2054+
}
2055+
}
2056+
if _, err := e.Table("action").Where("repo_id = ?", issue.RepoID).In("op_type", ActionCreateIssue, ActionCreatePullRequest).
2057+
Where("content LIKE ?", strconv.FormatInt(issue.ID, 10)+"|%").Delete(&Action{}); err != nil {
2058+
return err
2059+
}
2060+
2061+
// find attachments related to this issue and remove them
2062+
var attachments []*repo_model.Attachment
2063+
if err := e.In("issue_id", issue.ID).Find(&attachments); err != nil {
2064+
return err
2065+
}
2066+
2067+
for i := range attachments {
2068+
admin_model.RemoveStorageWithNotice(ctx, storage.Attachments, "Delete issue attachment", attachments[i].RelativePath())
2069+
}
2070+
2071+
// delete all database data still assigned to this issue
2072+
if err := deleteInIssue(e, issue.ID,
2073+
&issues.ContentHistory{},
2074+
&Comment{},
2075+
&IssueLabel{},
2076+
&IssueDependency{},
2077+
&IssueAssignees{},
2078+
&IssueUser{},
2079+
&Reaction{},
2080+
&IssueWatch{},
2081+
&Stopwatch{},
2082+
&TrackedTime{},
2083+
&ProjectIssue{},
2084+
&repo_model.Attachment{},
2085+
&PullRequest{},
2086+
); err != nil {
2087+
return err
2088+
}
2089+
2090+
// References to this issue in other issues
2091+
if _, err := e.In("ref_issue_id", issue.ID).Delete(&Comment{}); err != nil {
2092+
return err
2093+
}
2094+
2095+
// Delete dependencies for issues in other repositories
2096+
if _, err := e.In("dependency_id", issue.ID).Delete(&IssueDependency{}); err != nil {
2097+
return err
2098+
}
2099+
2100+
// delete from dependent issues
2101+
if _, err := e.In("dependent_issue_id", issue.ID).Delete(&Comment{}); err != nil {
2102+
return err
2103+
}
2104+
2105+
return nil
2106+
}
2107+
19942108
// DependencyInfo represents high level information about an issue which is a dependency of another issue.
19952109
type DependencyInfo struct {
19962110
Issue `xorm:"extends"`

models/issue_comment.go

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1161,9 +1161,7 @@ func DeleteComment(comment *Comment) error {
11611161
}
11621162

11631163
func deleteComment(e db.Engine, comment *Comment) error {
1164-
if _, err := e.Delete(&Comment{
1165-
ID: comment.ID,
1166-
}); err != nil {
1164+
if _, err := e.ID(comment.ID).NoAutoCondition().Delete(comment); err != nil {
11671165
return err
11681166
}
11691167

models/issue_test.go

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -397,6 +397,58 @@ func TestIssue_InsertIssue(t *testing.T) {
397397

398398
}
399399

400+
func TestIssue_DeleteIssue(t *testing.T) {
401+
assert.NoError(t, unittest.PrepareTestDatabase())
402+
403+
issueIDs, err := GetIssueIDsByRepoID(1)
404+
assert.NoError(t, err)
405+
assert.EqualValues(t, 5, len(issueIDs))
406+
407+
issue := &Issue{
408+
RepoID: 1,
409+
ID: issueIDs[2],
410+
}
411+
412+
err = DeleteIssue(issue)
413+
assert.NoError(t, err)
414+
issueIDs, err = GetIssueIDsByRepoID(1)
415+
assert.NoError(t, err)
416+
assert.EqualValues(t, 4, len(issueIDs))
417+
418+
// check attachment removal
419+
attachments, err := repo_model.GetAttachmentsByIssueID(4)
420+
assert.NoError(t, err)
421+
issue, err = GetIssueByID(4)
422+
assert.NoError(t, err)
423+
err = DeleteIssue(issue)
424+
assert.NoError(t, err)
425+
assert.EqualValues(t, 2, len(attachments))
426+
for i := range attachments {
427+
attachment, err := repo_model.GetAttachmentByUUID(attachments[i].UUID)
428+
assert.Error(t, err)
429+
assert.True(t, repo_model.IsErrAttachmentNotExist(err))
430+
assert.Nil(t, attachment)
431+
}
432+
433+
// check issue dependencies
434+
user, err := user_model.GetUserByID(1)
435+
assert.NoError(t, err)
436+
issue1, err := GetIssueByID(1)
437+
assert.NoError(t, err)
438+
issue2, err := GetIssueByID(2)
439+
assert.NoError(t, err)
440+
err = CreateIssueDependency(user, issue1, issue2)
441+
assert.NoError(t, err)
442+
left, err := IssueNoDependenciesLeft(issue1)
443+
assert.NoError(t, err)
444+
assert.False(t, left)
445+
err = DeleteIssue(&Issue{ID: 2})
446+
assert.NoError(t, err)
447+
left, err = IssueNoDependenciesLeft(issue1)
448+
assert.NoError(t, err)
449+
assert.True(t, left)
450+
}
451+
400452
func TestIssue_ResolveMentions(t *testing.T) {
401453
assert.NoError(t, unittest.PrepareTestDatabase())
402454

modules/notification/base/notifier.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ type Notifier interface {
2424

2525
NotifyNewIssue(issue *models.Issue, mentions []*user_model.User)
2626
NotifyIssueChangeStatus(*user_model.User, *models.Issue, *models.Comment, bool)
27+
NotifyDeleteIssue(*user_model.User, *models.Issue)
2728
NotifyIssueChangeMilestone(doer *user_model.User, issue *models.Issue, oldMilestoneID int64)
2829
NotifyIssueChangeAssignee(doer *user_model.User, issue *models.Issue, assignee *user_model.User, removed bool, comment *models.Comment)
2930
NotifyPullReviewRequest(doer *user_model.User, issue *models.Issue, reviewer *user_model.User, isRequest bool, comment *models.Comment)

modules/notification/base/null.go

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,10 @@ func (*NullNotifier) NotifyNewIssue(issue *models.Issue, mentions []*user_model.
3636
func (*NullNotifier) NotifyIssueChangeStatus(doer *user_model.User, issue *models.Issue, actionComment *models.Comment, isClosed bool) {
3737
}
3838

39+
// NotifyDeleteIssue notify when some issue deleted
40+
func (*NullNotifier) NotifyDeleteIssue(doer *user_model.User, issue *models.Issue) {
41+
}
42+
3943
// NotifyNewPullRequest places a place holder function
4044
func (*NullNotifier) NotifyNewPullRequest(pr *models.PullRequest, mentions []*user_model.User) {
4145
}

modules/notification/notification.go

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,13 @@ func NotifyIssueChangeStatus(doer *user_model.User, issue *models.Issue, actionC
6161
}
6262
}
6363

64+
// NotifyDeleteIssue notify when some issue deleted
65+
func NotifyDeleteIssue(doer *user_model.User, issue *models.Issue) {
66+
for _, notifier := range notifiers {
67+
notifier.NotifyDeleteIssue(doer, issue)
68+
}
69+
}
70+
6471
// NotifyMergePullRequest notifies merge pull request to notifiers
6572
func NotifyMergePullRequest(pr *models.PullRequest, doer *user_model.User) {
6673
for _, notifier := range notifiers {

routers/api/v1/api.go

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -835,7 +835,8 @@ func Routes(sessioner func(http.Handler) http.Handler) *web.Route {
835835
})
836836
m.Group("/{index}", func() {
837837
m.Combo("").Get(repo.GetIssue).
838-
Patch(reqToken(), bind(api.EditIssueOption{}), repo.EditIssue)
838+
Patch(reqToken(), bind(api.EditIssueOption{}), repo.EditIssue).
839+
Delete(reqToken(), reqAdmin(), repo.DeleteIssue)
839840
m.Group("/comments", func() {
840841
m.Combo("").Get(repo.ListIssueComments).
841842
Post(reqToken(), mustNotBeArchived, bind(api.CreateIssueCommentOption{}), repo.CreateIssueComment)

routers/api/v1/repo/issue.go

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -834,6 +834,52 @@ func EditIssue(ctx *context.APIContext) {
834834
ctx.JSON(http.StatusCreated, convert.ToAPIIssue(issue))
835835
}
836836

837+
func DeleteIssue(ctx *context.APIContext) {
838+
// swagger:operation DELETE /repos/{owner}/{repo}/issues/{index} issue issueDelete
839+
// ---
840+
// summary: Delete an issue
841+
// parameters:
842+
// - name: owner
843+
// in: path
844+
// description: owner of the repo
845+
// type: string
846+
// required: true
847+
// - name: repo
848+
// in: path
849+
// description: name of the repo
850+
// type: string
851+
// required: true
852+
// - name: index
853+
// in: path
854+
// description: index of issue to delete
855+
// type: integer
856+
// format: int64
857+
// required: true
858+
// responses:
859+
// "204":
860+
// "$ref": "#/responses/empty"
861+
// "403":
862+
// "$ref": "#/responses/forbidden"
863+
// "404":
864+
// "$ref": "#/responses/notFound"
865+
issue, err := models.GetIssueByIndex(ctx.Repo.Repository.ID, ctx.ParamsInt64(":index"))
866+
if err != nil {
867+
if models.IsErrIssueNotExist(err) {
868+
ctx.NotFound(err)
869+
} else {
870+
ctx.Error(http.StatusInternalServerError, "GetIssueByID", err)
871+
}
872+
return
873+
}
874+
875+
if err = issue_service.DeleteIssue(ctx.User, ctx.Repo.GitRepo, issue); err != nil {
876+
ctx.Error(http.StatusInternalServerError, "DeleteIssueByID", err)
877+
return
878+
}
879+
880+
ctx.Status(http.StatusNoContent)
881+
}
882+
837883
// UpdateIssueDeadline updates an issue deadline
838884
func UpdateIssueDeadline(ctx *context.APIContext) {
839885
// swagger:operation POST /repos/{owner}/{repo}/issues/{index}/deadline issue issueEditIssueDeadline

services/issue/issue.go

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,8 @@
55
package issue
66

77
import (
8+
"fmt"
9+
810
"code.gitea.io/gitea/models"
911
"code.gitea.io/gitea/models/db"
1012
repo_model "code.gitea.io/gitea/models/repo"
@@ -125,6 +127,33 @@ func UpdateAssignees(issue *models.Issue, oneAssignee string, multipleAssignees
125127
return
126128
}
127129

130+
// DeleteIssue deletes an issue
131+
func DeleteIssue(doer *user_model.User, gitRepo *git.Repository, issue *models.Issue) error {
132+
// load issue before deleting it
133+
if err := issue.LoadAttributes(); err != nil {
134+
return err
135+
}
136+
if err := issue.LoadPullRequest(); err != nil {
137+
return err
138+
}
139+
140+
// delete entries in database
141+
if err := models.DeleteIssue(issue); err != nil {
142+
return err
143+
}
144+
145+
// delete pull request related git data
146+
if issue.IsPull {
147+
if err := gitRepo.RemoveReference(fmt.Sprintf("%s%d", git.PullPrefix, issue.PullRequest.Index)); err != nil {
148+
return err
149+
}
150+
}
151+
152+
notification.NotifyDeleteIssue(doer, issue)
153+
154+
return nil
155+
}
156+
128157
// AddAssigneeIfNotAssigned adds an assignee only if he isn't already assigned to the issue.
129158
// Also checks for access of assigned user
130159
func AddAssigneeIfNotAssigned(issue *models.Issue, doer *user_model.User, assigneeID int64) (err error) {

templates/swagger/v1_json.tmpl

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4922,6 +4922,48 @@
49224922
}
49234923
}
49244924
},
4925+
"delete": {
4926+
"tags": [
4927+
"issue"
4928+
],
4929+
"summary": "Delete an issue",
4930+
"operationId": "issueDelete",
4931+
"parameters": [
4932+
{
4933+
"type": "string",
4934+
"description": "owner of the repo",
4935+
"name": "owner",
4936+
"in": "path",
4937+
"required": true
4938+
},
4939+
{
4940+
"type": "string",
4941+
"description": "name of the repo",
4942+
"name": "repo",
4943+
"in": "path",
4944+
"required": true
4945+
},
4946+
{
4947+
"type": "integer",
4948+
"format": "int64",
4949+
"description": "index of issue to delete",
4950+
"name": "index",
4951+
"in": "path",
4952+
"required": true
4953+
}
4954+
],
4955+
"responses": {
4956+
"204": {
4957+
"$ref": "#/responses/empty"
4958+
},
4959+
"403": {
4960+
"$ref": "#/responses/forbidden"
4961+
},
4962+
"404": {
4963+
"$ref": "#/responses/notFound"
4964+
}
4965+
}
4966+
},
49254967
"patch": {
49264968
"consumes": [
49274969
"application/json"

0 commit comments

Comments
 (0)