diff --git a/options/locale/locale_en-US.ini b/options/locale/locale_en-US.ini index 03017ce6746df..047ec74837e24 100644 --- a/options/locale/locale_en-US.ini +++ b/options/locale/locale_en-US.ini @@ -1639,6 +1639,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` @@ -1959,7 +1964,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 4e7f245296c17..cf88b6582a343 100644 --- a/routers/web/repo/issue_comment.go +++ b/routers/web/repo/issue_comment.go @@ -11,6 +11,7 @@ import ( "strconv" "strings" + 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" @@ -22,6 +23,7 @@ import ( repo_module "code.gitea.io/gitea/modules/repository" "code.gitea.io/gitea/modules/setting" api "code.gitea.io/gitea/modules/structs" + "code.gitea.io/gitea/modules/util" "code.gitea.io/gitea/modules/web" "code.gitea.io/gitea/services/context" "code.gitea.io/gitea/services/convert" @@ -30,6 +32,142 @@ import ( pull_service "code.gitea.io/gitea/services/pull" ) +func reopenPullWithComment(ctx *context.Context, issue *issues_model.Issue, content string, attachments []string) *issues_model.Comment { + pull := issue.PullRequest + + // 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 nil + } + + // 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 nil + } + prHeadCommitID, err := git.GetFullCommitID(ctx, pull.BaseRepo.RepoPath(), prHeadRef) + if err != nil { + ctx.ServerError("Get head commit Id of pr fail", err) + return nil + } + + if ok := gitrepo.IsBranchExist(ctx, pull.HeadRepo, pull.BaseBranch); !ok { + // todo localize + ctx.JSONError("The origin branch is delete, cannot reopen.") + return nil + } + headBranchRef := git.RefNameFromBranch(pull.HeadBranch) + headBranchCommitID, err := git.GetFullCommitID(ctx, pull.HeadRepo.RepoPath(), headBranchRef.String()) + if err != nil { + ctx.ServerError("Get head commit Id of head branch fail", err) + return nil + } + + err = pull.LoadIssue(ctx) + if err != nil { + ctx.ServerError("load the issue of pull request error", err) + return nil + } + + 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 nil + } + } + } + + branchExist, err := git_model.IsBranchExist(ctx, pull.HeadRepo.ID, pull.HeadBranch) + if err != nil { + ctx.ServerError("IsBranchExist", err) + return nil + } + if !branchExist { + ctx.JSONError(ctx.Tr("repo.pulls.head_branch_not_exist")) + return nil + } + + // 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 nil + } + } + if pr != nil { + ctx.Flash.Info(ctx.Tr("repo.pulls.open_unmerged_pull_exists", pr.Index)) + return nil + } + + createdComment, err := issue_service.ReopenIssueWithComment(ctx, issue, ctx.Doer, "", content, attachments) + if err != nil { + if errors.Is(err, user_model.ErrBlockedUser) { + ctx.JSONError(ctx.Tr("repo.issues.comment.blocked_user")) + } else { + ctx.ServerError("ReopenIssue", err) + } + return nil + } + + // 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 nil + } + prHeadCommitID, err := ctx.Repo.GitRepo.GetRefCommitID(prHeadRef) + if err != nil { + ctx.ServerError("Get head commit Id of pr fail", err) + return nil + } + + headBranchCommitID, err := gitrepo.GetBranchCommitID(ctx, pull.HeadRepo, pull.HeadBranch) + if err != nil { + ctx.ServerError("Get head commit Id of head branch fail", err) + return nil + } + + if err = pull.LoadIssue(ctx); err != nil { + ctx.ServerError("load the issue of pull request error", err) + return nil + } + + // 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 nil + } + } + + // Regenerate patch and test conflict. + pull.HeadCommitID = "" + pull_service.StartPullRequestCheckImmediately(ctx, pull) + } + return createdComment +} + // NewComment create a comment for issue func NewComment(ctx *context.Context) { form := web.GetForm(ctx).(*forms.CreateCommentForm) @@ -66,6 +204,11 @@ func NewComment(ctx *context.Context) { return } + if form.Content == "" { + ctx.JSONError(ctx.Tr("repo.issues.comment.empty_content")) + return + } + var attachments []string if setting.Attachment.Enabled { attachments = form.Files @@ -76,132 +219,61 @@ 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 - - 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) - } + var createdComment *issues_model.Comment + var err error - // 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 - } - - // 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 := git.RefNameFromBranch(pull.HeadBranch) - headBranchCommitID, err := git.GetFullCommitID(ctx, pull.HeadRepo.RepoPath(), headBranchRef.String()) - if err != nil { - ctx.ServerError("Get head commit Id of head branch fail", err) - return - } - - err = pull.LoadIssue(ctx) - if err != nil { - ctx.ServerError("load the issue of pull request error", err) - 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 - } - } - } - } + switch form.Status { + case "reopen": + if !issue.IsClosed { + ctx.JSONError(ctx.Tr("repo.issues.not_closed")) + 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 + } - if pr != nil { - ctx.Flash.Info(ctx.Tr("repo.pulls.open_unmerged_pull_exists", pr.Index)) - } 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) - } + if issue.IsPull { + createdComment = reopenPullWithComment(ctx, issue, form.Content, attachments) + } else { + 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 { + ctx.ServerError("ReopenIssue", err) } + return } } + if ctx.Written() { + return + } + 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")) @@ -211,7 +283,15 @@ 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 := util.Iif(issue.IsPull, "pulls", "issues") + + 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..50a26547357ac 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 } + +// ReopenIssueWithComment 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 +} diff --git a/tests/integration/issue_test.go b/tests/integration/issue_test.go index 613353e55cbb9..09292b52b66e9 100644 --- a/tests/integration/issue_test.go +++ b/tests/integration/issue_test.go @@ -255,6 +255,26 @@ func TestIssueCommentClose(t *testing.T) { htmlDoc := NewHTMLParser(t, resp.Body) val := htmlDoc.doc.Find(".comment-list .comment .render-content p").First().Text() assert.Equal(t, "Description", val) + val = strings.TrimSpace(htmlDoc.doc.Find(".issue-title-header .issue-state-label").Text()) + assert.Equal(t, "Closed", val) +} + +func TestIssueCommentReopen(t *testing.T) { + defer tests.PrepareTestEnv(t)() + session := loginUser(t, "user2") + issueURL := testNewIssue(t, session, "user2", "repo1", "Title", "Description") + testIssueAddComment(t, session, issueURL, "Test comment 1", "") + testIssueAddComment(t, session, issueURL, "Test comment 2", "close") + testIssueAddComment(t, session, issueURL, "Test comment 2", "reopen") + + // Validate that issue content has not been updated + req := NewRequest(t, "GET", issueURL) + resp := session.MakeRequest(t, req, http.StatusOK) + htmlDoc := NewHTMLParser(t, resp.Body) + val := htmlDoc.doc.Find(".comment-list .comment .render-content p").First().Text() + assert.Equal(t, "Description", val) + val = strings.TrimSpace(htmlDoc.doc.Find(".issue-title-header .issue-state-label").Text()) + assert.Equal(t, "Open", val) } func TestIssueCommentDelete(t *testing.T) { diff --git a/tests/integration/pull_create_test.go b/tests/integration/pull_create_test.go index 2eb5e94cf9d98..e546f3a7c5803 100644 --- a/tests/integration/pull_create_test.go +++ b/tests/integration/pull_create_test.go @@ -315,3 +315,49 @@ func TestCreatePullWhenBlocked(t *testing.T) { MakeRequest(t, req, http.StatusNoContent) }) } + +func TestPullRequestCommentClose(t *testing.T) { + onGiteaRun(t, func(t *testing.T, u *url.URL) { + session := loginUser(t, "user1") + testRepoFork(t, session, "user2", "repo1", "user1", "repo1", "") + testEditFile(t, session, "user1", "repo1", "master", "README.md", "Hello, World (Edited)\n") + resp := testPullCreate(t, session, "user1", "repo1", false, "master", "master", "This is a pull title") + pullURL := test.RedirectURL(resp) + + testIssueAddComment(t, session, pullURL, "Test comment 1", "") + testIssueAddComment(t, session, pullURL, "Test comment 2", "") + testIssueAddComment(t, session, pullURL, "Test comment 3", "close") + + // Validate that issue content has not been updated + req := NewRequest(t, "GET", pullURL) + resp = session.MakeRequest(t, req, http.StatusOK) + htmlDoc := NewHTMLParser(t, resp.Body) + val := strings.Split(strings.TrimSpace(htmlDoc.doc.Find("#issue-title-display > h1").First().Text()), "\n")[0] + assert.Equal(t, "This is a pull title", val) + val = strings.TrimSpace(htmlDoc.doc.Find(".issue-title-header .issue-state-label").Text()) + assert.Equal(t, "Closed", val) + }) +} + +func TestPullRequestCommentReopen(t *testing.T) { + onGiteaRun(t, func(t *testing.T, u *url.URL) { + session := loginUser(t, "user1") + testRepoFork(t, session, "user2", "repo1", "user1", "repo1", "") + testEditFile(t, session, "user1", "repo1", "master", "README.md", "Hello, World (Edited)\n") + resp := testPullCreate(t, session, "user1", "repo1", false, "master", "master", "This is a pull title") + pullURL := test.RedirectURL(resp) + + testIssueAddComment(t, session, pullURL, "Test comment 1", "") + testIssueAddComment(t, session, pullURL, "Test comment 2", "close") + testIssueAddComment(t, session, pullURL, "Test comment 2", "reopen") + + // Validate that issue content has not been updated + req := NewRequest(t, "GET", pullURL) + resp = session.MakeRequest(t, req, http.StatusOK) + htmlDoc := NewHTMLParser(t, resp.Body) + val := strings.Split(strings.TrimSpace(htmlDoc.doc.Find("#issue-title-display > h1").First().Text()), "\n")[0] + assert.Equal(t, "This is a pull title", val) + val = strings.TrimSpace(htmlDoc.doc.Find(".issue-title-header .issue-state-label").Text()) + assert.Equal(t, "Open", val) + }) +}