Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 7 additions & 1 deletion options/locale/locale_en-US.ini
Original file line number Diff line number Diff line change
Expand Up @@ -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 <a id="%[1]s" href="#%[1]s">%[2]s</a>`
issues.reopened_at = `reopened this issue <a id="%[1]s" href="#%[1]s">%[2]s</a>`
issues.commit_ref_at = `referenced this issue from a commit <a id="%[1]s" href="#%[1]s">%[2]s</a>`
Expand Down Expand Up @@ -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
Expand Down
312 changes: 196 additions & 116 deletions routers/web/repo/issue_comment.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand All @@ -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"
Expand All @@ -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 <refs/pulls/pr_index/head> 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 <refs/pulls/pr_index/head> 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)
Expand Down Expand Up @@ -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
Expand All @@ -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 <refs/pulls/pr_index/head> 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"))
Expand All @@ -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
Expand Down
Loading
Loading