mirror of
				https://github.com/go-gitea/gitea.git
				synced 2025-10-26 12:27:06 +00:00 
			
		
		
		
	Use CloseIssue and ReopenIssue instead of ChangeStatus (#32467)
				
					
				
			The behaviors of closing issues and reopening issues are very different. So splitting it into two different functions makes it easier to maintain. - [x] Split ChangeIssueStatus into CloseIssue and ReopenIssue both at the service layer and model layer - [x] Rename `isClosed` to `CloseOrReopen` to make it more readable. - [x] Add transactions for ReopenIssue and CloseIssue --------- Co-authored-by: Zettat123 <zettat123@gmail.com>
This commit is contained in:
		| @@ -49,9 +49,13 @@ func TestCreateIssueDependency(t *testing.T) { | ||||
| 	assert.False(t, left) | ||||
|  | ||||
| 	// Close #2 and check again | ||||
| 	_, err = issues_model.ChangeIssueStatus(db.DefaultContext, issue2, user1, true) | ||||
| 	_, err = issues_model.CloseIssue(db.DefaultContext, issue2, user1) | ||||
| 	assert.NoError(t, err) | ||||
|  | ||||
| 	issue2Closed, err := issues_model.GetIssueByID(db.DefaultContext, 2) | ||||
| 	assert.NoError(t, err) | ||||
| 	assert.True(t, issue2Closed.IsClosed) | ||||
|  | ||||
| 	left, err = issues_model.IssueNoDependenciesLeft(db.DefaultContext, issue1) | ||||
| 	assert.NoError(t, err) | ||||
| 	assert.True(t, left) | ||||
| @@ -59,4 +63,11 @@ func TestCreateIssueDependency(t *testing.T) { | ||||
| 	// Test removing the dependency | ||||
| 	err = issues_model.RemoveIssueDependency(db.DefaultContext, user1, issue1, issue2, issues_model.DependencyTypeBlockedBy) | ||||
| 	assert.NoError(t, err) | ||||
|  | ||||
| 	_, err = issues_model.ReopenIssue(db.DefaultContext, issue2, user1) | ||||
| 	assert.NoError(t, err) | ||||
|  | ||||
| 	issue2Reopened, err := issues_model.GetIssueByID(db.DefaultContext, 2) | ||||
| 	assert.NoError(t, err) | ||||
| 	assert.False(t, issue2Reopened.IsClosed) | ||||
| } | ||||
|   | ||||
| @@ -119,8 +119,8 @@ func doChangeIssueStatus(ctx context.Context, issue *Issue, doer *user_model.Use | ||||
| 	}) | ||||
| } | ||||
|  | ||||
| // ChangeIssueStatus changes issue status to open or closed. | ||||
| func ChangeIssueStatus(ctx context.Context, issue *Issue, doer *user_model.User, isClosed bool) (*Comment, error) { | ||||
| // CloseIssue changes issue status to closed. | ||||
| func CloseIssue(ctx context.Context, issue *Issue, doer *user_model.User) (*Comment, error) { | ||||
| 	if err := issue.LoadRepo(ctx); err != nil { | ||||
| 		return nil, err | ||||
| 	} | ||||
| @@ -128,7 +128,45 @@ func ChangeIssueStatus(ctx context.Context, issue *Issue, doer *user_model.User, | ||||
| 		return nil, err | ||||
| 	} | ||||
|  | ||||
| 	return changeIssueStatus(ctx, issue, doer, isClosed, false) | ||||
| 	ctx, committer, err := db.TxContext(ctx) | ||||
| 	if err != nil { | ||||
| 		return nil, err | ||||
| 	} | ||||
| 	defer committer.Close() | ||||
|  | ||||
| 	comment, err := changeIssueStatus(ctx, issue, doer, true, false) | ||||
| 	if err != nil { | ||||
| 		return nil, err | ||||
| 	} | ||||
| 	if err := committer.Commit(); err != nil { | ||||
| 		return nil, err | ||||
| 	} | ||||
| 	return comment, nil | ||||
| } | ||||
|  | ||||
| // ReopenIssue changes issue status to open. | ||||
| func ReopenIssue(ctx context.Context, issue *Issue, doer *user_model.User) (*Comment, error) { | ||||
| 	if err := issue.LoadRepo(ctx); err != nil { | ||||
| 		return nil, err | ||||
| 	} | ||||
| 	if err := issue.LoadPoster(ctx); err != nil { | ||||
| 		return nil, err | ||||
| 	} | ||||
|  | ||||
| 	ctx, committer, err := db.TxContext(ctx) | ||||
| 	if err != nil { | ||||
| 		return nil, err | ||||
| 	} | ||||
| 	defer committer.Close() | ||||
|  | ||||
| 	comment, err := changeIssueStatus(ctx, issue, doer, false, false) | ||||
| 	if err != nil { | ||||
| 		return nil, err | ||||
| 	} | ||||
| 	if err := committer.Commit(); err != nil { | ||||
| 		return nil, err | ||||
| 	} | ||||
| 	return comment, nil | ||||
| } | ||||
|  | ||||
| // ChangeIssueTitle changes the title of this issue, as the given user. | ||||
|   | ||||
| @@ -98,7 +98,7 @@ func TestXRef_ResolveCrossReferences(t *testing.T) { | ||||
| 	i1 := testCreateIssue(t, 1, 2, "title1", "content1", false) | ||||
| 	i2 := testCreateIssue(t, 1, 2, "title2", "content2", false) | ||||
| 	i3 := testCreateIssue(t, 1, 2, "title3", "content3", false) | ||||
| 	_, err := issues_model.ChangeIssueStatus(db.DefaultContext, i3, d, true) | ||||
| 	_, err := issues_model.CloseIssue(db.DefaultContext, i3, d) | ||||
| 	assert.NoError(t, err) | ||||
|  | ||||
| 	pr := testCreatePR(t, 1, 2, "titlepr", fmt.Sprintf("closes #%d", i1.Index)) | ||||
|   | ||||
| @@ -733,7 +733,7 @@ func CreateIssue(ctx *context.APIContext) { | ||||
| 	} | ||||
|  | ||||
| 	if form.Closed { | ||||
| 		if err := issue_service.ChangeStatus(ctx, issue, ctx.Doer, "", true); err != nil { | ||||
| 		if err := issue_service.CloseIssue(ctx, issue, ctx.Doer, ""); err != nil { | ||||
| 			if issues_model.IsErrDependenciesLeft(err) { | ||||
| 				ctx.Error(http.StatusPreconditionFailed, "DependenciesLeft", "cannot close this issue because it still has open dependencies") | ||||
| 				return | ||||
| @@ -912,27 +912,11 @@ func EditIssue(ctx *context.APIContext) { | ||||
| 			} | ||||
| 		} | ||||
|  | ||||
| 		var isClosed bool | ||||
| 		switch state := api.StateType(*form.State); state { | ||||
| 		case api.StateOpen: | ||||
| 			isClosed = false | ||||
| 		case api.StateClosed: | ||||
| 			isClosed = true | ||||
| 		default: | ||||
| 			ctx.Error(http.StatusPreconditionFailed, "UnknownIssueStateError", fmt.Sprintf("unknown state: %s", state)) | ||||
| 		state := api.StateType(*form.State) | ||||
| 		closeOrReopenIssue(ctx, issue, state) | ||||
| 		if ctx.Written() { | ||||
| 			return | ||||
| 		} | ||||
|  | ||||
| 		if issue.IsClosed != isClosed { | ||||
| 			if err := issue_service.ChangeStatus(ctx, issue, ctx.Doer, "", isClosed); err != nil { | ||||
| 				if issues_model.IsErrDependenciesLeft(err) { | ||||
| 					ctx.Error(http.StatusPreconditionFailed, "DependenciesLeft", "cannot close this issue because it still has open dependencies") | ||||
| 					return | ||||
| 				} | ||||
| 				ctx.Error(http.StatusInternalServerError, "ChangeStatus", err) | ||||
| 				return | ||||
| 			} | ||||
| 		} | ||||
| 	} | ||||
|  | ||||
| 	// Refetch from database to assign some automatic values | ||||
| @@ -1055,3 +1039,26 @@ func UpdateIssueDeadline(ctx *context.APIContext) { | ||||
|  | ||||
| 	ctx.JSON(http.StatusCreated, api.IssueDeadline{Deadline: deadlineUnix.AsTimePtr()}) | ||||
| } | ||||
|  | ||||
| func closeOrReopenIssue(ctx *context.APIContext, issue *issues_model.Issue, state api.StateType) { | ||||
| 	if state != api.StateOpen && state != api.StateClosed { | ||||
| 		ctx.Error(http.StatusPreconditionFailed, "UnknownIssueStateError", fmt.Sprintf("unknown state: %s", state)) | ||||
| 		return | ||||
| 	} | ||||
|  | ||||
| 	if state == api.StateClosed && !issue.IsClosed { | ||||
| 		if err := issue_service.CloseIssue(ctx, issue, ctx.Doer, ""); err != nil { | ||||
| 			if issues_model.IsErrDependenciesLeft(err) { | ||||
| 				ctx.Error(http.StatusPreconditionFailed, "DependenciesLeft", "cannot close this issue or pull request because it still has open dependencies") | ||||
| 				return | ||||
| 			} | ||||
| 			ctx.Error(http.StatusInternalServerError, "CloseIssue", err) | ||||
| 			return | ||||
| 		} | ||||
| 	} else if state == api.StateOpen && issue.IsClosed { | ||||
| 		if err := issue_service.ReopenIssue(ctx, issue, ctx.Doer, ""); err != nil { | ||||
| 			ctx.Error(http.StatusInternalServerError, "ReopenIssue", err) | ||||
| 			return | ||||
| 		} | ||||
| 	} | ||||
| } | ||||
|   | ||||
| @@ -728,27 +728,11 @@ func EditPullRequest(ctx *context.APIContext) { | ||||
| 			return | ||||
| 		} | ||||
|  | ||||
| 		var isClosed bool | ||||
| 		switch state := api.StateType(*form.State); state { | ||||
| 		case api.StateOpen: | ||||
| 			isClosed = false | ||||
| 		case api.StateClosed: | ||||
| 			isClosed = true | ||||
| 		default: | ||||
| 			ctx.Error(http.StatusPreconditionFailed, "UnknownPRStateError", fmt.Sprintf("unknown state: %s", state)) | ||||
| 		state := api.StateType(*form.State) | ||||
| 		closeOrReopenIssue(ctx, issue, state) | ||||
| 		if ctx.Written() { | ||||
| 			return | ||||
| 		} | ||||
|  | ||||
| 		if issue.IsClosed != isClosed { | ||||
| 			if err := issue_service.ChangeStatus(ctx, issue, ctx.Doer, "", isClosed); err != nil { | ||||
| 				if issues_model.IsErrDependenciesLeft(err) { | ||||
| 					ctx.Error(http.StatusPreconditionFailed, "DependenciesLeft", "cannot close this pull request because it still has open dependencies") | ||||
| 					return | ||||
| 				} | ||||
| 				ctx.Error(http.StatusInternalServerError, "ChangeStatus", err) | ||||
| 				return | ||||
| 			} | ||||
| 		} | ||||
| 	} | ||||
|  | ||||
| 	// change pull target branch | ||||
|   | ||||
| @@ -154,25 +154,28 @@ func NewComment(ctx *context.Context) { | ||||
| 			if pr != nil { | ||||
| 				ctx.Flash.Info(ctx.Tr("repo.pulls.open_unmerged_pull_exists", pr.Index)) | ||||
| 			} else { | ||||
| 				isClosed := form.Status == "close" | ||||
| 				if err := issue_service.ChangeStatus(ctx, issue, ctx.Doer, "", isClosed); err != nil { | ||||
| 					log.Error("ChangeStatus: %v", err) | ||||
|  | ||||
| 					if issues_model.IsErrDependenciesLeft(err) { | ||||
| 						if issue.IsPull { | ||||
| 							ctx.JSONError(ctx.Tr("repo.issues.dependency.pr_close_blocked")) | ||||
| 						} else { | ||||
| 							ctx.JSONError(ctx.Tr("repo.issues.dependency.issue_close_blocked")) | ||||
| 				if form.Status == "close" && !issue.IsClosed { | ||||
| 					if err := issue_service.CloseIssue(ctx, issue, ctx.Doer, ""); err != nil { | ||||
| 						log.Error("CloseIssue: %v", err) | ||||
| 						if issues_model.IsErrDependenciesLeft(err) { | ||||
| 							if issue.IsPull { | ||||
| 								ctx.JSONError(ctx.Tr("repo.issues.dependency.pr_close_blocked")) | ||||
| 							} else { | ||||
| 								ctx.JSONError(ctx.Tr("repo.issues.dependency.issue_close_blocked")) | ||||
| 							} | ||||
| 							return | ||||
| 						} | ||||
| 						return | ||||
| 					} else { | ||||
| 						if err := stopTimerIfAvailable(ctx, ctx.Doer, issue); err != nil { | ||||
| 							ctx.ServerError("stopTimerIfAvailable", err) | ||||
| 							return | ||||
| 						} | ||||
| 						log.Trace("Issue [%d] status changed to closed: %v", issue.ID, issue.IsClosed) | ||||
| 					} | ||||
| 				} else { | ||||
| 					if err := stopTimerIfAvailable(ctx, ctx.Doer, issue); err != nil { | ||||
| 						ctx.ServerError("CreateOrStopIssueStopwatch", err) | ||||
| 						return | ||||
| 				} else if form.Status == "reopen" && issue.IsClosed { | ||||
| 					if err := issue_service.ReopenIssue(ctx, issue, ctx.Doer, ""); err != nil { | ||||
| 						log.Error("ReopenIssue: %v", err) | ||||
| 					} | ||||
|  | ||||
| 					log.Trace("Issue [%d] status changed to closed: %v", issue.ID, issue.IsClosed) | ||||
| 				} | ||||
| 			} | ||||
| 		} | ||||
|   | ||||
| @@ -418,14 +418,11 @@ func UpdateIssueStatus(ctx *context.Context) { | ||||
| 		return | ||||
| 	} | ||||
|  | ||||
| 	var isClosed bool | ||||
| 	switch action := ctx.FormString("action"); action { | ||||
| 	case "open": | ||||
| 		isClosed = false | ||||
| 	case "close": | ||||
| 		isClosed = true | ||||
| 	default: | ||||
| 	action := ctx.FormString("action") | ||||
| 	if action != "open" && action != "close" { | ||||
| 		log.Warn("Unrecognized action: %s", action) | ||||
| 		ctx.JSONOK() | ||||
| 		return | ||||
| 	} | ||||
|  | ||||
| 	if _, err := issues.LoadRepositories(ctx); err != nil { | ||||
| @@ -441,15 +438,20 @@ func UpdateIssueStatus(ctx *context.Context) { | ||||
| 		if issue.IsPull && issue.PullRequest.HasMerged { | ||||
| 			continue | ||||
| 		} | ||||
| 		if issue.IsClosed != isClosed { | ||||
| 			if err := issue_service.ChangeStatus(ctx, issue, ctx.Doer, "", isClosed); err != nil { | ||||
| 		if action == "close" && !issue.IsClosed { | ||||
| 			if err := issue_service.CloseIssue(ctx, issue, ctx.Doer, ""); err != nil { | ||||
| 				if issues_model.IsErrDependenciesLeft(err) { | ||||
| 					ctx.JSON(http.StatusPreconditionFailed, map[string]any{ | ||||
| 						"error": ctx.Tr("repo.issues.dependency.issue_batch_close_blocked", issue.Index), | ||||
| 					}) | ||||
| 					return | ||||
| 				} | ||||
| 				ctx.ServerError("ChangeStatus", err) | ||||
| 				ctx.ServerError("CloseIssue", err) | ||||
| 				return | ||||
| 			} | ||||
| 		} else if action == "open" && issue.IsClosed { | ||||
| 			if err := issue_service.ReopenIssue(ctx, issue, ctx.Doer, ""); err != nil { | ||||
| 				ctx.ServerError("ReopenIssue", err) | ||||
| 				return | ||||
| 			} | ||||
| 		} | ||||
|   | ||||
| @@ -188,15 +188,19 @@ func UpdateIssuesCommit(ctx context.Context, doer *user_model.User, repo *repo_m | ||||
| 					continue | ||||
| 				} | ||||
| 			} | ||||
| 			isClosed := ref.Action == references.XRefActionCloses | ||||
| 			if isClosed && len(ref.TimeLog) > 0 { | ||||
| 				if err := issueAddTime(ctx, refIssue, doer, c.Timestamp, ref.TimeLog); err != nil { | ||||
|  | ||||
| 			refIssue.Repo = refRepo | ||||
| 			if ref.Action == references.XRefActionCloses && !refIssue.IsClosed { | ||||
| 				if len(ref.TimeLog) > 0 { | ||||
| 					if err := issueAddTime(ctx, refIssue, doer, c.Timestamp, ref.TimeLog); err != nil { | ||||
| 						return err | ||||
| 					} | ||||
| 				} | ||||
| 				if err := CloseIssue(ctx, refIssue, doer, c.Sha1); err != nil { | ||||
| 					return err | ||||
| 				} | ||||
| 			} | ||||
| 			if isClosed != refIssue.IsClosed { | ||||
| 				refIssue.Repo = refRepo | ||||
| 				if err := ChangeStatus(ctx, refIssue, doer, c.Sha1, isClosed); err != nil { | ||||
| 			} else if ref.Action == references.XRefActionReopens && refIssue.IsClosed { | ||||
| 				if err := ReopenIssue(ctx, refIssue, doer, c.Sha1); err != nil { | ||||
| 					return err | ||||
| 				} | ||||
| 			} | ||||
|   | ||||
| @@ -6,34 +6,54 @@ package issue | ||||
| import ( | ||||
| 	"context" | ||||
|  | ||||
| 	"code.gitea.io/gitea/models/db" | ||||
| 	issues_model "code.gitea.io/gitea/models/issues" | ||||
| 	user_model "code.gitea.io/gitea/models/user" | ||||
| 	"code.gitea.io/gitea/modules/log" | ||||
| 	notify_service "code.gitea.io/gitea/services/notify" | ||||
| ) | ||||
|  | ||||
| // ChangeStatus changes issue status to open or closed. | ||||
| // closed means the target status | ||||
| // Fix me: you should check whether the current issue status is same to the target status before call this function | ||||
| // as in function changeIssueStatus we will return WasClosedError, even the issue status and target status are both open | ||||
| func ChangeStatus(ctx context.Context, issue *issues_model.Issue, doer *user_model.User, commitID string, closed bool) error { | ||||
| 	comment, err := issues_model.ChangeIssueStatus(ctx, issue, doer, closed) | ||||
| // CloseIssue close an issue. | ||||
| func CloseIssue(ctx context.Context, issue *issues_model.Issue, doer *user_model.User, commitID string) error { | ||||
| 	dbCtx, committer, err := db.TxContext(ctx) | ||||
| 	if err != nil { | ||||
| 		if issues_model.IsErrDependenciesLeft(err) && closed { | ||||
| 			if err := issues_model.FinishIssueStopwatchIfPossible(ctx, doer, issue); err != nil { | ||||
| 		return err | ||||
| 	} | ||||
| 	defer committer.Close() | ||||
|  | ||||
| 	comment, err := issues_model.CloseIssue(dbCtx, issue, doer) | ||||
| 	if err != nil { | ||||
| 		if issues_model.IsErrDependenciesLeft(err) { | ||||
| 			if err := issues_model.FinishIssueStopwatchIfPossible(dbCtx, doer, issue); err != nil { | ||||
| 				log.Error("Unable to stop stopwatch for issue[%d]#%d: %v", issue.ID, issue.Index, err) | ||||
| 			} | ||||
| 		} | ||||
| 		return err | ||||
| 	} | ||||
|  | ||||
| 	if closed { | ||||
| 		if err := issues_model.FinishIssueStopwatchIfPossible(ctx, doer, issue); err != nil { | ||||
| 			return err | ||||
| 		} | ||||
| 	if err := issues_model.FinishIssueStopwatchIfPossible(dbCtx, doer, issue); err != nil { | ||||
| 		return err | ||||
| 	} | ||||
|  | ||||
| 	notify_service.IssueChangeStatus(ctx, doer, commitID, issue, comment, closed) | ||||
| 	if err := committer.Commit(); err != nil { | ||||
| 		return err | ||||
| 	} | ||||
| 	committer.Close() | ||||
|  | ||||
| 	notify_service.IssueChangeStatus(ctx, doer, commitID, issue, comment, true) | ||||
|  | ||||
| 	return nil | ||||
| } | ||||
|  | ||||
| // ReopenIssue reopen an issue. | ||||
| // FIXME: If some issues dependent this one are closed, should we also reopen them? | ||||
| func ReopenIssue(ctx context.Context, issue *issues_model.Issue, doer *user_model.User, commitID string) error { | ||||
| 	comment, err := issues_model.ReopenIssue(ctx, issue, doer) | ||||
| 	if err != nil { | ||||
| 		return err | ||||
| 	} | ||||
|  | ||||
| 	notify_service.IssueChangeStatus(ctx, doer, commitID, issue, comment, false) | ||||
|  | ||||
| 	return nil | ||||
| } | ||||
|   | ||||
| @@ -263,14 +263,17 @@ func handleCloseCrossReferences(ctx context.Context, pr *issues_model.PullReques | ||||
| 		if err = ref.Issue.LoadRepo(ctx); err != nil { | ||||
| 			return err | ||||
| 		} | ||||
| 		isClosed := ref.RefAction == references.XRefActionCloses | ||||
| 		if isClosed != ref.Issue.IsClosed { | ||||
| 			if err = issue_service.ChangeStatus(ctx, ref.Issue, doer, pr.MergedCommitID, isClosed); err != nil { | ||||
| 		if ref.RefAction == references.XRefActionCloses && !ref.Issue.IsClosed { | ||||
| 			if err = issue_service.CloseIssue(ctx, ref.Issue, doer, pr.MergedCommitID); err != nil { | ||||
| 				// Allow ErrDependenciesLeft | ||||
| 				if !issues_model.IsErrDependenciesLeft(err) { | ||||
| 					return err | ||||
| 				} | ||||
| 			} | ||||
| 		} else if ref.RefAction == references.XRefActionReopens && ref.Issue.IsClosed { | ||||
| 			if err = issue_service.ReopenIssue(ctx, ref.Issue, doer, pr.MergedCommitID); err != nil { | ||||
| 				return err | ||||
| 			} | ||||
| 		} | ||||
| 	} | ||||
| 	return nil | ||||
|   | ||||
| @@ -706,7 +706,7 @@ func CloseBranchPulls(ctx context.Context, doer *user_model.User, repoID int64, | ||||
|  | ||||
| 	var errs errlist | ||||
| 	for _, pr := range prs { | ||||
| 		if err = issue_service.ChangeStatus(ctx, pr.Issue, doer, "", true); err != nil && !issues_model.IsErrPullWasClosed(err) && !issues_model.IsErrDependenciesLeft(err) { | ||||
| 		if err = issue_service.CloseIssue(ctx, pr.Issue, doer, ""); err != nil && !issues_model.IsErrPullWasClosed(err) && !issues_model.IsErrDependenciesLeft(err) { | ||||
| 			errs = append(errs, err) | ||||
| 		} | ||||
| 	} | ||||
| @@ -740,7 +740,7 @@ func CloseRepoBranchesPulls(ctx context.Context, doer *user_model.User, repo *re | ||||
| 			if pr.BaseRepoID == repo.ID { | ||||
| 				continue | ||||
| 			} | ||||
| 			if err = issue_service.ChangeStatus(ctx, pr.Issue, doer, "", true); err != nil && !issues_model.IsErrPullWasClosed(err) { | ||||
| 			if err = issue_service.CloseIssue(ctx, pr.Issue, doer, ""); err != nil && !issues_model.IsErrPullWasClosed(err) { | ||||
| 				errs = append(errs, err) | ||||
| 			} | ||||
| 		} | ||||
|   | ||||
		Reference in New Issue
	
	Block a user
	 Lunny Xiao
					Lunny Xiao