mirror of
https://codeberg.org/forgejo/forgejo.git
synced 2024-12-01 05:36:19 +01:00
[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>
This commit is contained in:
parent
6859b69198
commit
062fd4c217
11 changed files with 299 additions and 4 deletions
114
models/issue.go
114
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"`
|
||||
|
|
|
@ -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"
|
||||
|
|
Loading…
Reference in a new issue