Skip to content

Commit d7b6ab9

Browse files
fnetXlunnyGusted6543
authored and
Stelios Malathouras
committed
[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 70391a6 commit d7b6ab9

File tree

11 files changed

+299
-4
lines changed

11 files changed

+299
-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"
@@ -24,6 +25,7 @@ import (
2425
"code.gitea.io/gitea/modules/log"
2526
"code.gitea.io/gitea/modules/references"
2627
"code.gitea.io/gitea/modules/setting"
28+
"code.gitea.io/gitea/modules/storage"
2729
api "code.gitea.io/gitea/modules/structs"
2830
"code.gitea.io/gitea/modules/timeutil"
2931
"code.gitea.io/gitea/modules/util"
@@ -1990,6 +1992,118 @@ func UpdateIssueDeadline(issue *Issue, deadlineUnix timeutil.TimeStamp, doer *us
19901992
return committer.Commit()
19911993
}
19921994

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

models/issue_comment.go

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

11541154
func deleteComment(e db.Engine, comment *Comment) error {
1155-
if _, err := e.Delete(&Comment{
1156-
ID: comment.ID,
1157-
}); err != nil {
1155+
if _, err := e.ID(comment.ID).NoAutoCondition().Delete(comment); err != nil {
11581156
return err
11591157
}
11601158

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
assert.NoError(t, err)
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/nosql/manager_leveldb.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import (
1111
"strings"
1212

1313
"code.gitea.io/gitea/modules/log"
14+
1415
"github.com/syndtr/goleveldb/leveldb"
1516
"github.com/syndtr/goleveldb/leveldb/errors"
1617
"github.com/syndtr/goleveldb/leveldb/opt"

modules/notification/base/notifier.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ type Notifier interface {
2222
NotifyTransferRepository(doer *user_model.User, repo *repo_model.Repository, oldOwnerName string)
2323
NotifyNewIssue(issue *models.Issue, mentions []*user_model.User)
2424
NotifyIssueChangeStatus(*user_model.User, *models.Issue, *models.Comment, bool)
25+
NotifyDeleteIssue(*user_model.User, *models.Issue)
2526
NotifyIssueChangeMilestone(doer *user_model.User, issue *models.Issue, oldMilestoneID int64)
2627
NotifyIssueChangeAssignee(doer *user_model.User, issue *models.Issue, assignee *user_model.User, removed bool, comment *models.Comment)
2728
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
@@ -33,6 +33,10 @@ func (*NullNotifier) NotifyNewIssue(issue *models.Issue, mentions []*user_model.
3333
func (*NullNotifier) NotifyIssueChangeStatus(doer *user_model.User, issue *models.Issue, actionComment *models.Comment, isClosed bool) {
3434
}
3535

36+
// NotifyDeleteIssue notify when some issue deleted
37+
func (*NullNotifier) NotifyDeleteIssue(doer *user_model.User, issue *models.Issue) {
38+
}
39+
3640
// NotifyNewPullRequest places a place holder function
3741
func (*NullNotifier) NotifyNewPullRequest(pr *models.PullRequest, mentions []*user_model.User) {
3842
}

modules/notification/notification.go

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

63+
// NotifyDeleteIssue notify when some issue deleted
64+
func NotifyDeleteIssue(doer *user_model.User, issue *models.Issue) {
65+
for _, notifier := range notifiers {
66+
notifier.NotifyDeleteIssue(doer, issue)
67+
}
68+
}
69+
6370
// NotifyMergePullRequest notifies merge pull request to notifiers
6471
func NotifyMergePullRequest(pr *models.PullRequest, doer *user_model.User) {
6572
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) {

0 commit comments

Comments
 (0)