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>tags/v1.18.0-dev
@@ -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"` |
@@ -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 | |||
} | |||
@@ -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()) | |||
@@ -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" |
@@ -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) |
@@ -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) { | |||
} |
@@ -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 { |
@@ -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) |
@@ -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 |
@@ -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) { |
@@ -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" |