diff options
author | Lunny Xiao <xiaolunwen@gmail.com> | 2024-12-24 23:38:30 -0800 |
---|---|---|
committer | GitHub <noreply@github.com> | 2024-12-25 07:38:30 +0000 |
commit | 5feb1a6bff6b6931ebe197258032557f55e32c6c (patch) | |
tree | 48e3aeb50c12c6bbffa86b0bea7cc9b22c1baf51 | |
parent | f44712f22bc7bfce049c64c27f60453ff1e41a5c (diff) | |
download | gitea-5feb1a6bff6b6931ebe197258032557f55e32c6c.tar.gz gitea-5feb1a6bff6b6931ebe197258032557f55e32c6c.zip |
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>
-rw-r--r-- | models/issues/dependency_test.go | 13 | ||||
-rw-r--r-- | models/issues/issue_update.go | 44 | ||||
-rw-r--r-- | models/issues/issue_xref_test.go | 2 | ||||
-rw-r--r-- | routers/api/v1/repo/issue.go | 47 | ||||
-rw-r--r-- | routers/api/v1/repo/pull.go | 22 | ||||
-rw-r--r-- | routers/web/repo/issue_comment.go | 35 | ||||
-rw-r--r-- | routers/web/repo/issue_list.go | 22 | ||||
-rw-r--r-- | services/issue/commit.go | 18 | ||||
-rw-r--r-- | services/issue/status.go | 46 | ||||
-rw-r--r-- | services/pull/merge.go | 9 | ||||
-rw-r--r-- | services/pull/pull.go | 4 |
11 files changed, 167 insertions, 95 deletions
diff --git a/models/issues/dependency_test.go b/models/issues/dependency_test.go index 6eed483cc9..67418039de 100644 --- a/models/issues/dependency_test.go +++ b/models/issues/dependency_test.go @@ -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) } diff --git a/models/issues/issue_update.go b/models/issues/issue_update.go index 5b929c9115..ceb4a4027e 100644 --- a/models/issues/issue_update.go +++ b/models/issues/issue_update.go @@ -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. diff --git a/models/issues/issue_xref_test.go b/models/issues/issue_xref_test.go index f1b1bb2a6b..7f257330b7 100644 --- a/models/issues/issue_xref_test.go +++ b/models/issues/issue_xref_test.go @@ -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)) diff --git a/routers/api/v1/repo/issue.go b/routers/api/v1/repo/issue.go index 6f45fce226..86dbcee5f7 100644 --- a/routers/api/v1/repo/issue.go +++ b/routers/api/v1/repo/issue.go @@ -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 + } + } +} diff --git a/routers/api/v1/repo/pull.go b/routers/api/v1/repo/pull.go index ca5120eef5..d0c3459b63 100644 --- a/routers/api/v1/repo/pull.go +++ b/routers/api/v1/repo/pull.go @@ -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 diff --git a/routers/web/repo/issue_comment.go b/routers/web/repo/issue_comment.go index 438822431d..8564c613de 100644 --- a/routers/web/repo/issue_comment.go +++ b/routers/web/repo/issue_comment.go @@ -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) } } } diff --git a/routers/web/repo/issue_list.go b/routers/web/repo/issue_list.go index 2c73a24632..2f615a100e 100644 --- a/routers/web/repo/issue_list.go +++ b/routers/web/repo/issue_list.go @@ -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 } } diff --git a/services/issue/commit.go b/services/issue/commit.go index 0579e0f5c5..963d0359fd 100644 --- a/services/issue/commit.go +++ b/services/issue/commit.go @@ -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 } } diff --git a/services/issue/status.go b/services/issue/status.go index 967c29bd22..e18b891175 100644 --- a/services/issue/status.go +++ b/services/issue/status.go @@ -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 + } + + 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, closed) + notify_service.IssueChangeStatus(ctx, doer, commitID, issue, comment, false) return nil } diff --git a/services/pull/merge.go b/services/pull/merge.go index fba85f1e51..75011a697c 100644 --- a/services/pull/merge.go +++ b/services/pull/merge.go @@ -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 diff --git a/services/pull/pull.go b/services/pull/pull.go index 0256c2c3f6..ac91fd6409 100644 --- a/services/pull/pull.go +++ b/services/pull/pull.go @@ -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) } } |