diff --git a/options/locale/locale_en-US.ini b/options/locale/locale_en-US.ini
index b3eb7b1f4a4fd..4caf8dd45414b 100644
--- a/options/locale/locale_en-US.ini
+++ b/options/locale/locale_en-US.ini
@@ -1638,6 +1638,11 @@ issues.reopen_issue = Reopen
issues.reopen_comment_issue = Reopen with Comment
issues.create_comment = Comment
issues.comment.blocked_user = Cannot create or edit comment because you are blocked by the poster or repository owner.
+issues.not_closed = The issue is not closed.
+issues.reopen_not_allowed = No permission to reopen this issue.
+issues.reopen_not_allowed_merged = A pull request cannot be reopened after it has been merged.
+issues.comment.empty_content = The comment content cannot be empty.
+issues.already_closed = The issue is already closed.
issues.closed_at = `closed this issue %[2]s`
issues.reopened_at = `reopened this issue %[2]s`
issues.commit_ref_at = `referenced this issue from a commit %[2]s`
@@ -1958,7 +1963,8 @@ pulls.has_merged = Failed: The pull request has been merged. You cannot merge ag
pulls.push_rejected = Push Failed: The push was rejected. Review the Git Hooks for this repository.
pulls.push_rejected_summary = Full Rejection Message
pulls.push_rejected_no_message = Push Failed: The push was rejected but there was no remote message. Review the Git Hooks for this repository.
-pulls.open_unmerged_pull_exists = `You cannot perform a reopen operation because there is a pending pull request (#%d) with identical properties.`
+pulls.open_unmerged_pull_exists = You cannot perform a reopen operation because there is a pending pull request (#%d) with identical properties.
+pulls.head_branch_not_exist = The head branch does not exist, cannot reopen the pull request.
pulls.status_checking = Some checks are pending
pulls.status_checks_success = All checks were successful
pulls.status_checks_warning = Some checks reported warnings
diff --git a/routers/web/repo/issue_comment.go b/routers/web/repo/issue_comment.go
index cb5b2d801952d..d0ea9fc061947 100644
--- a/routers/web/repo/issue_comment.go
+++ b/routers/web/repo/issue_comment.go
@@ -10,11 +10,11 @@ import (
"net/http"
"strconv"
+ git_model "code.gitea.io/gitea/models/git"
issues_model "code.gitea.io/gitea/models/issues"
"code.gitea.io/gitea/models/renderhelper"
user_model "code.gitea.io/gitea/models/user"
"code.gitea.io/gitea/modules/git"
- "code.gitea.io/gitea/modules/gitrepo"
"code.gitea.io/gitea/modules/log"
"code.gitea.io/gitea/modules/markup/markdown"
repo_module "code.gitea.io/gitea/modules/repository"
@@ -74,132 +74,140 @@ func NewComment(ctx *context.Context) {
return
}
- var comment *issues_model.Comment
- defer func() {
- // Check if issue admin/poster changes the status of issue.
- if (ctx.Repo.CanWriteIssuesOrPulls(issue.IsPull) || (ctx.IsSigned && issue.IsPoster(ctx.Doer.ID))) &&
- (form.Status == "reopen" || form.Status == "close") &&
- !(issue.IsPull && issue.PullRequest.HasMerged) {
- // Duplication and conflict check should apply to reopen pull request.
- var pr *issues_model.PullRequest
+ var createdComment *issues_model.Comment
+ var err error
- if form.Status == "reopen" && issue.IsPull {
- pull := issue.PullRequest
- var err error
- pr, err = issues_model.GetUnmergedPullRequest(ctx, pull.HeadRepoID, pull.BaseRepoID, pull.HeadBranch, pull.BaseBranch, pull.Flow)
- if err != nil {
- if !issues_model.IsErrPullRequestNotExist(err) {
- ctx.JSONError(ctx.Tr("repo.issues.dependency.pr_close_blocked"))
- return
- }
- }
-
- // Regenerate patch and test conflict.
- if pr == nil {
- issue.PullRequest.HeadCommitID = ""
- pull_service.StartPullRequestCheckImmediately(ctx, issue.PullRequest)
- }
+ switch form.Status {
+ case "reopen":
+ if !issue.IsClosed {
+ ctx.JSONError(ctx.Tr("repo.issues.not_closed"))
+ return
+ }
- // check whether the ref of PR in base repo is consistent with the head commit of head branch in the head repo
- // get head commit of PR
- if pull.Flow == issues_model.PullRequestFlowGithub {
- prHeadRef := pull.GetGitHeadRefName()
- if err := pull.LoadBaseRepo(ctx); err != nil {
- ctx.ServerError("Unable to load base repo", err)
- return
- }
- prHeadCommitID, err := git.GetFullCommitID(ctx, pull.BaseRepo.RepoPath(), prHeadRef)
- if err != nil {
- ctx.ServerError("Get head commit Id of pr fail", err)
- return
- }
+ if !ctx.Repo.CanWriteIssuesOrPulls(issue.IsPull) &&
+ !issue.IsPoster(ctx.Doer.ID) &&
+ !ctx.Doer.IsAdmin {
+ ctx.JSONError(ctx.Tr("repo.issues.reopen_not_allowed"))
+ return
+ }
- // get head commit of branch in the head repo
- if err := pull.LoadHeadRepo(ctx); err != nil {
- ctx.ServerError("Unable to load head repo", err)
- return
- }
- if ok := gitrepo.IsBranchExist(ctx, pull.HeadRepo, pull.BaseBranch); !ok {
- // todo localize
- ctx.JSONError("The origin branch is delete, cannot reopen.")
- return
- }
- headBranchRef := pull.GetGitHeadBranchRefName()
- headBranchCommitID, err := git.GetFullCommitID(ctx, pull.HeadRepo.RepoPath(), headBranchRef)
- if err != nil {
- ctx.ServerError("Get head commit Id of head branch fail", err)
- return
- }
+ if issue.IsPull {
+ pull := issue.PullRequest
+ if pull.HasMerged {
+ ctx.JSONError(ctx.Tr("repo.issues.reopen_not_allowed_merged"))
+ return
+ }
- err = pull.LoadIssue(ctx)
- if err != nil {
- ctx.ServerError("load the issue of pull request error", err)
- return
- }
+ // get head commit of branch in the head repo
+ if err := pull.LoadHeadRepo(ctx); err != nil {
+ ctx.ServerError("Unable to load head repo", err)
+ return
+ }
+ branchExist, err := git_model.IsBranchExist(ctx, pull.HeadRepo.ID, pull.HeadBranch)
+ if err != nil {
+ ctx.ServerError("IsBranchExist", err)
+ return
+ }
+ if !branchExist {
+ ctx.JSONError(ctx.Tr("repo.pulls.head_branch_not_exist"))
+ return
+ }
- if prHeadCommitID != headBranchCommitID {
- // force push to base repo
- err := git.Push(ctx, pull.HeadRepo.RepoPath(), git.PushOptions{
- Remote: pull.BaseRepo.RepoPath(),
- Branch: pull.HeadBranch + ":" + prHeadRef,
- Force: true,
- Env: repo_module.InternalPushingEnvironment(pull.Issue.Poster, pull.BaseRepo),
- })
- if err != nil {
- ctx.ServerError("force push error", err)
- return
- }
- }
+ // check if an opened pull request exists with the same head branch and base branch
+ pr, err := issues_model.GetUnmergedPullRequest(ctx, pull.HeadRepoID, pull.BaseRepoID, pull.HeadBranch, pull.BaseBranch, pull.Flow)
+ if err != nil {
+ if !issues_model.IsErrPullRequestNotExist(err) {
+ ctx.JSONError(err.Error())
+ return
}
}
-
if pr != nil {
ctx.Flash.Info(ctx.Tr("repo.pulls.open_unmerged_pull_exists", pr.Index))
+ return
+ }
+ }
+
+ createdComment, err = issue_service.ReopenIssueWithComment(ctx, issue, ctx.Doer, "", form.Content, attachments)
+ if err != nil {
+ if errors.Is(err, user_model.ErrBlockedUser) {
+ ctx.JSONError(ctx.Tr("repo.issues.comment.blocked_user"))
} else {
- 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
- }
- } 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 form.Status == "reopen" && issue.IsClosed {
- if err := issue_service.ReopenIssue(ctx, issue, ctx.Doer, ""); err != nil {
- log.Error("ReopenIssue: %v", err)
+ ctx.ServerError("ReopenIssue", err)
+ }
+ return
+ }
+
+ if issue.IsPull {
+ pull := issue.PullRequest
+ // check whether the ref of PR in base repo is consistent with the head commit of head branch in the head repo
+ // get head commit of PR
+ if pull.Flow == issues_model.PullRequestFlowGithub {
+ prHeadRef := pull.GetGitHeadRefName()
+ if err := pull.LoadBaseRepo(ctx); err != nil {
+ ctx.ServerError("Unable to load base repo", err)
+ return
+ }
+ prHeadCommitID, err := git.GetFullCommitID(ctx, pull.BaseRepo.RepoPath(), prHeadRef)
+ if err != nil {
+ ctx.ServerError("Get head commit Id of pr fail", err)
+ return
+ }
+
+ headBranchRef := pull.GetGitHeadBranchRefName()
+ headBranchCommitID, err := git.GetFullCommitID(ctx, pull.HeadRepo.RepoPath(), headBranchRef)
+ if err != nil {
+ ctx.ServerError("Get head commit Id of head branch fail", err)
+ return
+ }
+
+ if err = pull.LoadIssue(ctx); err != nil {
+ ctx.ServerError("load the issue of pull request error", err)
+ return
+ }
+
+ // if the head commit ID of the PR is different from the head branch
+ if prHeadCommitID != headBranchCommitID {
+ // force push to base repo
+ err := git.Push(ctx, pull.HeadRepo.RepoPath(), git.PushOptions{
+ Remote: pull.BaseRepo.RepoPath(),
+ Branch: pull.HeadBranch + ":" + prHeadRef,
+ Force: true,
+ Env: repo_module.InternalPushingEnvironment(pull.Issue.Poster, pull.BaseRepo),
+ })
+ if err != nil {
+ ctx.ServerError("force push error", err)
+ return
}
}
}
+
+ // Regenerate patch and test conflict.
+ pull.HeadCommitID = ""
+ pull_service.StartPullRequestCheckImmediately(ctx, pull)
+ }
+ case "close":
+ if issue.IsClosed {
+ ctx.JSONError(ctx.Tr("repo.issues.already_closed"))
+ return
}
- // Redirect to comment hashtag if there is any actual content.
- typeName := "issues"
- if issue.IsPull {
- typeName = "pulls"
+ if !ctx.Repo.CanWriteIssuesOrPulls(issue.IsPull) &&
+ !issue.IsPoster(ctx.Doer.ID) &&
+ !ctx.Doer.IsAdmin {
+ ctx.JSONError(ctx.Tr("repo.issues.close_not_allowed"))
+ return
}
- if comment != nil {
- ctx.JSONRedirect(fmt.Sprintf("%s/%s/%d#%s", ctx.Repo.RepoLink, typeName, issue.Index, comment.HashTag()))
- } else {
- ctx.JSONRedirect(fmt.Sprintf("%s/%s/%d", ctx.Repo.RepoLink, typeName, issue.Index))
+
+ createdComment, err = issue_service.CloseIssueWithComment(ctx, issue, ctx.Doer, "", form.Content, attachments)
+ default:
+ if len(form.Content) == 0 && len(attachments) == 0 {
+ ctx.JSONError(ctx.Tr("repo.issues.comment.empty_content"))
+ return
}
- }()
- // Fix #321: Allow empty comments, as long as we have attachments.
- if len(form.Content) == 0 && len(attachments) == 0 {
- return
+ createdComment, err = issue_service.CreateIssueComment(ctx, ctx.Doer, ctx.Repo.Repository, issue, form.Content, attachments)
}
- comment, err := issue_service.CreateIssueComment(ctx, ctx.Doer, ctx.Repo.Repository, issue, form.Content, attachments)
if err != nil {
if errors.Is(err, user_model.ErrBlockedUser) {
ctx.JSONError(ctx.Tr("repo.issues.comment.blocked_user"))
@@ -209,7 +217,17 @@ func NewComment(ctx *context.Context) {
return
}
- log.Trace("Comment created: %d/%d/%d", ctx.Repo.Repository.ID, issue.ID, comment.ID)
+ // Redirect to comment hashtag if there is any actual content.
+ typeName := "issues"
+ if issue.IsPull {
+ typeName = "pulls"
+ }
+ if createdComment != nil {
+ log.Trace("Comment created: %d/%d/%d", ctx.Repo.Repository.ID, issue.ID, createdComment.ID)
+ ctx.JSONRedirect(fmt.Sprintf("%s/%s/%d#%s", ctx.Repo.RepoLink, typeName, issue.Index, createdComment.HashTag()))
+ } else {
+ ctx.JSONRedirect(fmt.Sprintf("%s/%s/%d", ctx.Repo.RepoLink, typeName, issue.Index))
+ }
}
// UpdateCommentContent change comment of issue's content
diff --git a/services/issue/comments.go b/services/issue/comments.go
index 9442701029b57..bbeb75f27244e 100644
--- a/services/issue/comments.go
+++ b/services/issue/comments.go
@@ -15,6 +15,7 @@ import (
user_model "code.gitea.io/gitea/models/user"
"code.gitea.io/gitea/modules/gitrepo"
"code.gitea.io/gitea/modules/json"
+ "code.gitea.io/gitea/modules/log"
"code.gitea.io/gitea/modules/timeutil"
git_service "code.gitea.io/gitea/services/git"
notify_service "code.gitea.io/gitea/services/notify"
@@ -55,6 +56,22 @@ func CreateRefComment(ctx context.Context, doer *user_model.User, repo *repo_mod
return err
}
+func notifyCommentCreated(ctx context.Context, doer *user_model.User, repo *repo_model.Repository, issue *issues_model.Issue, comment *issues_model.Comment) error {
+ mentions, err := issues_model.FindAndUpdateIssueMentions(ctx, issue, doer, comment.Content)
+ if err != nil {
+ return err
+ }
+
+ // reload issue to ensure it has the latest data, especially the number of comments
+ issue, err = issues_model.GetIssueByID(ctx, issue.ID)
+ if err != nil {
+ return err
+ }
+
+ notify_service.CreateIssueComment(ctx, doer, repo, issue, comment, mentions)
+ return nil
+}
+
// CreateIssueComment creates a plain issue comment.
func CreateIssueComment(ctx context.Context, doer *user_model.User, repo *repo_model.Repository, issue *issues_model.Issue, content string, attachments []string) (*issues_model.Comment, error) {
if user_model.IsUserBlockedBy(ctx, doer, issue.PosterID, repo.OwnerID) {
@@ -75,19 +92,11 @@ func CreateIssueComment(ctx context.Context, doer *user_model.User, repo *repo_m
return nil, err
}
- mentions, err := issues_model.FindAndUpdateIssueMentions(ctx, issue, doer, comment.Content)
- if err != nil {
- return nil, err
+ if err := notifyCommentCreated(ctx, doer, repo, issue, comment); err != nil {
+ // If notification fails, we still return the comment but log the error.
+ log.Error("Failed to notify comment creation: %v", err)
}
- // reload issue to ensure it has the latest data, especially the number of comments
- issue, err = issues_model.GetIssueByID(ctx, issue.ID)
- if err != nil {
- return nil, err
- }
-
- notify_service.CreateIssueComment(ctx, doer, repo, issue, comment, mentions)
-
return comment, nil
}
diff --git a/services/issue/status.go b/services/issue/status.go
index fa59df93ba107..06d9ad5f6426b 100644
--- a/services/issue/status.go
+++ b/services/issue/status.go
@@ -13,7 +13,7 @@ import (
notify_service "code.gitea.io/gitea/services/notify"
)
-// CloseIssue close an issue.
+// CloseIssue closes an issue
func CloseIssue(ctx context.Context, issue *issues_model.Issue, doer *user_model.User, commitID string) error {
var comment *issues_model.Comment
if err := db.WithTx(ctx, func(ctx context.Context) error {
@@ -39,7 +39,53 @@ func CloseIssue(ctx context.Context, issue *issues_model.Issue, doer *user_model
return nil
}
-// ReopenIssue reopen an issue.
+// CloseIssueWithComment close an issue with comment
+func CloseIssueWithComment(ctx context.Context, issue *issues_model.Issue, doer *user_model.User, commitID, commentContent string, attachments []string) (*issues_model.Comment, error) {
+ var refComment, createdComment *issues_model.Comment
+ if err := db.WithTx(ctx, func(ctx context.Context) error {
+ var err error
+ if commentContent != "" || len(attachments) > 0 {
+ createdComment, err = issues_model.CreateComment(ctx, &issues_model.CreateCommentOptions{
+ Type: issues_model.CommentTypeComment,
+ Doer: doer,
+ Repo: issue.Repo,
+ Issue: issue,
+ Content: commentContent,
+ Attachments: attachments,
+ })
+ if err != nil {
+ return err
+ }
+ }
+
+ refComment, err = issues_model.CloseIssue(ctx, issue, doer)
+ if err != nil {
+ if issues_model.IsErrDependenciesLeft(err) {
+ if _, err := issues_model.FinishIssueStopwatch(ctx, doer, issue); err != nil {
+ log.Error("Unable to stop stopwatch for issue[%d]#%d: %v", issue.ID, issue.Index, err)
+ }
+ }
+ return err
+ }
+
+ _, err = issues_model.FinishIssueStopwatch(ctx, doer, issue)
+ return err
+ }); err != nil {
+ return nil, err
+ }
+
+ if createdComment != nil {
+ if err := notifyCommentCreated(ctx, doer, issue.Repo, issue, createdComment); err != nil {
+ log.Error("Unable to notify comment created for issue[%d]#%d: %v", issue.ID, issue.Index, err)
+ }
+ }
+
+ notify_service.IssueChangeStatus(ctx, doer, commitID, issue, refComment, true)
+
+ return createdComment, 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)
@@ -51,3 +97,40 @@ func ReopenIssue(ctx context.Context, issue *issues_model.Issue, doer *user_mode
return nil
}
+
+// ReopenIssue reopen an issue with a comment.
+// FIXME: If some issues dependent this one are closed, should we also reopen them?
+func ReopenIssueWithComment(ctx context.Context, issue *issues_model.Issue, doer *user_model.User, commitID, commentContent string, attachments []string) (*issues_model.Comment, error) {
+ var createdComment *issues_model.Comment
+ refComment, err := db.WithTx2(ctx, func(ctx context.Context) (*issues_model.Comment, error) {
+ var err error
+ if commentContent != "" || len(attachments) > 0 {
+ createdComment, err = issues_model.CreateComment(ctx, &issues_model.CreateCommentOptions{
+ Type: issues_model.CommentTypeComment,
+ Doer: doer,
+ Repo: issue.Repo,
+ Issue: issue,
+ Content: commentContent,
+ Attachments: attachments,
+ })
+ if err != nil {
+ return nil, err
+ }
+ }
+
+ return issues_model.ReopenIssue(ctx, issue, doer)
+ })
+ if err != nil {
+ return nil, err
+ }
+
+ if createdComment != nil {
+ if err := notifyCommentCreated(ctx, doer, issue.Repo, issue, createdComment); err != nil {
+ log.Error("Unable to notify comment created for issue[%d]#%d: %v", issue.ID, issue.Index, err)
+ }
+ }
+
+ notify_service.IssueChangeStatus(ctx, doer, commitID, issue, refComment, false)
+
+ return createdComment, nil
+}