mirror of
				https://github.com/go-gitea/gitea.git
				synced 2025-10-29 10:57:44 +09: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
						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