| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483 |
- // Copyright 2024 The Gitea Authors. All rights reserved.
- // SPDX-License-Identifier: MIT
-
- package repo
-
- import (
- "errors"
- "fmt"
- "html/template"
- "net/http"
- "strconv"
- "strings"
-
- 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/htmlutil"
- "code.gitea.io/gitea/modules/log"
- "code.gitea.io/gitea/modules/markup/markdown"
- 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/web"
- "code.gitea.io/gitea/services/context"
- "code.gitea.io/gitea/services/convert"
- "code.gitea.io/gitea/services/forms"
- issue_service "code.gitea.io/gitea/services/issue"
- pull_service "code.gitea.io/gitea/services/pull"
- )
-
- // NewComment create a comment for issue
- func NewComment(ctx *context.Context) {
- form := web.GetForm(ctx).(*forms.CreateCommentForm)
- issue := GetActionIssue(ctx)
- if ctx.Written() {
- return
- }
-
- if !ctx.IsSigned || (ctx.Doer.ID != issue.PosterID && !ctx.Repo.CanReadIssuesOrPulls(issue.IsPull)) {
- if log.IsTrace() {
- if ctx.IsSigned {
- issueType := "issues"
- if issue.IsPull {
- issueType = "pulls"
- }
- log.Trace("Permission Denied: User %-v not the Poster (ID: %d) and cannot read %s in Repo %-v.\n"+
- "User in Repo has Permissions: %-+v",
- ctx.Doer,
- issue.PosterID,
- issueType,
- ctx.Repo.Repository,
- ctx.Repo.Permission)
- } else {
- log.Trace("Permission Denied: Not logged in")
- }
- }
-
- ctx.HTTPError(http.StatusForbidden)
- return
- }
-
- if issue.IsLocked && !ctx.Repo.CanWriteIssuesOrPulls(issue.IsPull) && !ctx.Doer.IsAdmin {
- ctx.JSONError(ctx.Tr("repo.issues.comment_on_locked"))
- return
- }
-
- var attachments []string
- if setting.Attachment.Enabled {
- attachments = form.Files
- }
-
- if ctx.HasError() {
- ctx.JSONError(ctx.GetErrMsg())
- 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)
- }
-
- // 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 := 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
- }
-
- 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
- }
- }
- }
- }
-
- 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)
- }
- }
- }
- }
-
- // Redirect to comment hashtag if there is any actual content.
- typeName := "issues"
- if issue.IsPull {
- typeName = "pulls"
- }
- 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))
- }
- }()
-
- // Fix #321: Allow empty comments, as long as we have attachments.
- if len(form.Content) == 0 && len(attachments) == 0 {
- return
- }
-
- 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"))
- } else {
- ctx.ServerError("CreateIssueComment", err)
- }
- return
- }
-
- log.Trace("Comment created: %d/%d/%d", ctx.Repo.Repository.ID, issue.ID, comment.ID)
- }
-
- // UpdateCommentContent change comment of issue's content
- func UpdateCommentContent(ctx *context.Context) {
- comment, err := issues_model.GetCommentByID(ctx, ctx.PathParamInt64("id"))
- if err != nil {
- ctx.NotFoundOrServerError("GetCommentByID", issues_model.IsErrCommentNotExist, err)
- return
- }
-
- if err := comment.LoadIssue(ctx); err != nil {
- ctx.NotFoundOrServerError("LoadIssue", issues_model.IsErrIssueNotExist, err)
- return
- }
-
- if comment.Issue.RepoID != ctx.Repo.Repository.ID {
- ctx.NotFound(issues_model.ErrCommentNotExist{})
- return
- }
-
- if !ctx.IsSigned || (ctx.Doer.ID != comment.PosterID && !ctx.Repo.CanWriteIssuesOrPulls(comment.Issue.IsPull)) {
- ctx.HTTPError(http.StatusForbidden)
- return
- }
-
- if !comment.Type.HasContentSupport() {
- ctx.HTTPError(http.StatusNoContent)
- return
- }
-
- newContent := ctx.FormString("content")
- contentVersion := ctx.FormInt("content_version")
- if contentVersion != comment.ContentVersion {
- ctx.JSONError(ctx.Tr("repo.comments.edit.already_changed"))
- return
- }
-
- if newContent != comment.Content {
- // allow to save empty content
- oldContent := comment.Content
- comment.Content = newContent
-
- if err = issue_service.UpdateComment(ctx, comment, contentVersion, ctx.Doer, oldContent); err != nil {
- if errors.Is(err, user_model.ErrBlockedUser) {
- ctx.JSONError(ctx.Tr("repo.issues.comment.blocked_user"))
- } else if errors.Is(err, issues_model.ErrCommentAlreadyChanged) {
- ctx.JSONError(ctx.Tr("repo.comments.edit.already_changed"))
- } else {
- ctx.ServerError("UpdateComment", err)
- }
- return
- }
- }
-
- if err := comment.LoadAttachments(ctx); err != nil {
- ctx.ServerError("LoadAttachments", err)
- return
- }
-
- // when the update request doesn't intend to update attachments (eg: change checkbox state), ignore attachment updates
- if !ctx.FormBool("ignore_attachments") {
- if err := updateAttachments(ctx, comment, ctx.FormStrings("files[]")); err != nil {
- ctx.ServerError("UpdateAttachments", err)
- return
- }
- }
-
- var renderedContent template.HTML
- if comment.Content != "" {
- rctx := renderhelper.NewRenderContextRepoComment(ctx, ctx.Repo.Repository, renderhelper.RepoCommentOptions{
- FootnoteContextID: strconv.FormatInt(comment.ID, 10),
- })
- renderedContent, err = markdown.RenderString(rctx, comment.Content)
- if err != nil {
- ctx.ServerError("RenderString", err)
- return
- }
- }
-
- if strings.TrimSpace(string(renderedContent)) == "" {
- renderedContent = htmlutil.HTMLFormat(`<span class="no-content">%s</span>`, ctx.Tr("repo.issues.no_content"))
- }
-
- ctx.JSON(http.StatusOK, map[string]any{
- "content": renderedContent,
- "contentVersion": comment.ContentVersion,
- "attachments": attachmentsHTML(ctx, comment.Attachments, comment.Content),
- })
- }
-
- // DeleteComment delete comment of issue
- func DeleteComment(ctx *context.Context) {
- comment, err := issues_model.GetCommentByID(ctx, ctx.PathParamInt64("id"))
- if err != nil {
- ctx.NotFoundOrServerError("GetCommentByID", issues_model.IsErrCommentNotExist, err)
- return
- }
-
- if err := comment.LoadIssue(ctx); err != nil {
- ctx.NotFoundOrServerError("LoadIssue", issues_model.IsErrIssueNotExist, err)
- return
- }
-
- if comment.Issue.RepoID != ctx.Repo.Repository.ID {
- ctx.NotFound(issues_model.ErrCommentNotExist{})
- return
- }
-
- if !ctx.IsSigned || (ctx.Doer.ID != comment.PosterID && !ctx.Repo.CanWriteIssuesOrPulls(comment.Issue.IsPull)) {
- ctx.HTTPError(http.StatusForbidden)
- return
- } else if !comment.Type.HasContentSupport() {
- ctx.HTTPError(http.StatusNoContent)
- return
- }
-
- if err = issue_service.DeleteComment(ctx, ctx.Doer, comment); err != nil {
- ctx.ServerError("DeleteComment", err)
- return
- }
-
- ctx.Status(http.StatusOK)
- }
-
- // ChangeCommentReaction create a reaction for comment
- func ChangeCommentReaction(ctx *context.Context) {
- form := web.GetForm(ctx).(*forms.ReactionForm)
- comment, err := issues_model.GetCommentByID(ctx, ctx.PathParamInt64("id"))
- if err != nil {
- ctx.NotFoundOrServerError("GetCommentByID", issues_model.IsErrCommentNotExist, err)
- return
- }
-
- if err := comment.LoadIssue(ctx); err != nil {
- ctx.NotFoundOrServerError("LoadIssue", issues_model.IsErrIssueNotExist, err)
- return
- }
-
- if comment.Issue.RepoID != ctx.Repo.Repository.ID {
- ctx.NotFound(issues_model.ErrCommentNotExist{})
- return
- }
-
- if !ctx.IsSigned || (ctx.Doer.ID != comment.PosterID && !ctx.Repo.CanReadIssuesOrPulls(comment.Issue.IsPull)) {
- if log.IsTrace() {
- if ctx.IsSigned {
- issueType := "issues"
- if comment.Issue.IsPull {
- issueType = "pulls"
- }
- log.Trace("Permission Denied: User %-v not the Poster (ID: %d) and cannot read %s in Repo %-v.\n"+
- "User in Repo has Permissions: %-+v",
- ctx.Doer,
- comment.Issue.PosterID,
- issueType,
- ctx.Repo.Repository,
- ctx.Repo.Permission)
- } else {
- log.Trace("Permission Denied: Not logged in")
- }
- }
-
- ctx.HTTPError(http.StatusForbidden)
- return
- }
-
- if !comment.Type.HasContentSupport() {
- ctx.HTTPError(http.StatusNoContent)
- return
- }
-
- switch ctx.PathParam("action") {
- case "react":
- reaction, err := issue_service.CreateCommentReaction(ctx, ctx.Doer, comment, form.Content)
- if err != nil {
- if issues_model.IsErrForbiddenIssueReaction(err) || errors.Is(err, user_model.ErrBlockedUser) {
- ctx.ServerError("ChangeIssueReaction", err)
- return
- }
- log.Info("CreateCommentReaction: %s", err)
- break
- }
- // Reload new reactions
- comment.Reactions = nil
- if err = comment.LoadReactions(ctx, ctx.Repo.Repository); err != nil {
- log.Info("comment.LoadReactions: %s", err)
- break
- }
-
- log.Trace("Reaction for comment created: %d/%d/%d/%d", ctx.Repo.Repository.ID, comment.Issue.ID, comment.ID, reaction.ID)
- case "unreact":
- if err := issues_model.DeleteCommentReaction(ctx, ctx.Doer.ID, comment.Issue.ID, comment.ID, form.Content); err != nil {
- ctx.ServerError("DeleteCommentReaction", err)
- return
- }
-
- // Reload new reactions
- comment.Reactions = nil
- if err = comment.LoadReactions(ctx, ctx.Repo.Repository); err != nil {
- log.Info("comment.LoadReactions: %s", err)
- break
- }
-
- log.Trace("Reaction for comment removed: %d/%d/%d", ctx.Repo.Repository.ID, comment.Issue.ID, comment.ID)
- default:
- ctx.NotFound(nil)
- return
- }
-
- if len(comment.Reactions) == 0 {
- ctx.JSON(http.StatusOK, map[string]any{
- "empty": true,
- "html": "",
- })
- return
- }
-
- html, err := ctx.RenderToHTML(tplReactions, map[string]any{
- "ActionURL": fmt.Sprintf("%s/comments/%d/reactions", ctx.Repo.RepoLink, comment.ID),
- "Reactions": comment.Reactions.GroupByType(),
- })
- if err != nil {
- ctx.ServerError("ChangeCommentReaction.HTMLString", err)
- return
- }
- ctx.JSON(http.StatusOK, map[string]any{
- "html": html,
- })
- }
-
- // GetCommentAttachments returns attachments for the comment
- func GetCommentAttachments(ctx *context.Context) {
- comment, err := issues_model.GetCommentByID(ctx, ctx.PathParamInt64("id"))
- if err != nil {
- ctx.NotFoundOrServerError("GetCommentByID", issues_model.IsErrCommentNotExist, err)
- return
- }
-
- if err := comment.LoadIssue(ctx); err != nil {
- ctx.NotFoundOrServerError("LoadIssue", issues_model.IsErrIssueNotExist, err)
- return
- }
-
- if comment.Issue.RepoID != ctx.Repo.Repository.ID {
- ctx.NotFound(issues_model.ErrCommentNotExist{})
- return
- }
-
- if !ctx.Repo.Permission.CanReadIssuesOrPulls(comment.Issue.IsPull) {
- ctx.NotFound(issues_model.ErrCommentNotExist{})
- return
- }
-
- if !comment.Type.HasAttachmentSupport() {
- ctx.ServerError("GetCommentAttachments", fmt.Errorf("comment type %v does not support attachments", comment.Type))
- return
- }
-
- attachments := make([]*api.Attachment, 0)
- if err := comment.LoadAttachments(ctx); err != nil {
- ctx.ServerError("LoadAttachments", err)
- return
- }
- for i := 0; i < len(comment.Attachments); i++ {
- attachments = append(attachments, convert.ToAttachment(ctx.Repo.Repository, comment.Attachments[i]))
- }
- ctx.JSON(http.StatusOK, attachments)
- }
|