mirror of
				https://gitee.com/gitea/gitea
				synced 2025-11-04 16:40:24 +08: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:
		
				
					committed by
					
						
						GitHub
					
				
			
			
				
	
			
			
			
						parent
						
							6859b69198
						
					
				
				
					commit
					062fd4c217
				
			
							
								
								
									
										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"
 | 
			
		||||
 
 | 
			
		||||
		Reference in New Issue
	
	Block a user