You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

pull_review.go 8.2KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298
  1. // Copyright 2018 The Gitea Authors. All rights reserved.
  2. // SPDX-License-Identifier: MIT
  3. package repo
  4. import (
  5. "errors"
  6. "fmt"
  7. "net/http"
  8. issues_model "code.gitea.io/gitea/models/issues"
  9. pull_model "code.gitea.io/gitea/models/pull"
  10. "code.gitea.io/gitea/modules/base"
  11. "code.gitea.io/gitea/modules/context"
  12. "code.gitea.io/gitea/modules/json"
  13. "code.gitea.io/gitea/modules/log"
  14. "code.gitea.io/gitea/modules/setting"
  15. "code.gitea.io/gitea/modules/web"
  16. "code.gitea.io/gitea/services/forms"
  17. pull_service "code.gitea.io/gitea/services/pull"
  18. )
  19. const (
  20. tplConversation base.TplName = "repo/diff/conversation"
  21. tplNewComment base.TplName = "repo/diff/new_comment"
  22. )
  23. // RenderNewCodeCommentForm will render the form for creating a new review comment
  24. func RenderNewCodeCommentForm(ctx *context.Context) {
  25. issue := GetActionIssue(ctx)
  26. if ctx.Written() {
  27. return
  28. }
  29. if !issue.IsPull {
  30. return
  31. }
  32. currentReview, err := issues_model.GetCurrentReview(ctx, ctx.Doer, issue)
  33. if err != nil && !issues_model.IsErrReviewNotExist(err) {
  34. ctx.ServerError("GetCurrentReview", err)
  35. return
  36. }
  37. ctx.Data["PageIsPullFiles"] = true
  38. ctx.Data["Issue"] = issue
  39. ctx.Data["CurrentReview"] = currentReview
  40. pullHeadCommitID, err := ctx.Repo.GitRepo.GetRefCommitID(issue.PullRequest.GetGitRefName())
  41. if err != nil {
  42. ctx.ServerError("GetRefCommitID", err)
  43. return
  44. }
  45. ctx.Data["AfterCommitID"] = pullHeadCommitID
  46. ctx.HTML(http.StatusOK, tplNewComment)
  47. }
  48. // CreateCodeComment will create a code comment including an pending review if required
  49. func CreateCodeComment(ctx *context.Context) {
  50. form := web.GetForm(ctx).(*forms.CodeCommentForm)
  51. issue := GetActionIssue(ctx)
  52. if ctx.Written() {
  53. return
  54. }
  55. if !issue.IsPull {
  56. return
  57. }
  58. if ctx.HasError() {
  59. ctx.Flash.Error(ctx.Data["ErrorMsg"].(string))
  60. ctx.Redirect(fmt.Sprintf("%s/pulls/%d/files", ctx.Repo.RepoLink, issue.Index))
  61. return
  62. }
  63. signedLine := form.Line
  64. if form.Side == "previous" {
  65. signedLine *= -1
  66. }
  67. comment, err := pull_service.CreateCodeComment(ctx,
  68. ctx.Doer,
  69. ctx.Repo.GitRepo,
  70. issue,
  71. signedLine,
  72. form.Content,
  73. form.TreePath,
  74. !form.SingleReview,
  75. form.Reply,
  76. form.LatestCommitID,
  77. )
  78. if err != nil {
  79. ctx.ServerError("CreateCodeComment", err)
  80. return
  81. }
  82. if comment == nil {
  83. log.Trace("Comment not created: %-v #%d[%d]", ctx.Repo.Repository, issue.Index, issue.ID)
  84. ctx.Redirect(fmt.Sprintf("%s/pulls/%d/files", ctx.Repo.RepoLink, issue.Index))
  85. return
  86. }
  87. log.Trace("Comment created: %-v #%d[%d] Comment[%d]", ctx.Repo.Repository, issue.Index, issue.ID, comment.ID)
  88. if form.Origin == "diff" {
  89. renderConversation(ctx, comment)
  90. return
  91. }
  92. ctx.Redirect(comment.Link())
  93. }
  94. // UpdateResolveConversation add or remove an Conversation resolved mark
  95. func UpdateResolveConversation(ctx *context.Context) {
  96. origin := ctx.FormString("origin")
  97. action := ctx.FormString("action")
  98. commentID := ctx.FormInt64("comment_id")
  99. comment, err := issues_model.GetCommentByID(ctx, commentID)
  100. if err != nil {
  101. ctx.ServerError("GetIssueByID", err)
  102. return
  103. }
  104. if err = comment.LoadIssue(ctx); err != nil {
  105. ctx.ServerError("comment.LoadIssue", err)
  106. return
  107. }
  108. if comment.Issue.RepoID != ctx.Repo.Repository.ID {
  109. ctx.NotFound("comment's repoID is incorrect", errors.New("comment's repoID is incorrect"))
  110. return
  111. }
  112. var permResult bool
  113. if permResult, err = issues_model.CanMarkConversation(comment.Issue, ctx.Doer); err != nil {
  114. ctx.ServerError("CanMarkConversation", err)
  115. return
  116. }
  117. if !permResult {
  118. ctx.Error(http.StatusForbidden)
  119. return
  120. }
  121. if !comment.Issue.IsPull {
  122. ctx.Error(http.StatusBadRequest)
  123. return
  124. }
  125. if action == "Resolve" || action == "UnResolve" {
  126. err = issues_model.MarkConversation(comment, ctx.Doer, action == "Resolve")
  127. if err != nil {
  128. ctx.ServerError("MarkConversation", err)
  129. return
  130. }
  131. } else {
  132. ctx.Error(http.StatusBadRequest)
  133. return
  134. }
  135. if origin == "diff" {
  136. renderConversation(ctx, comment)
  137. return
  138. }
  139. ctx.JSON(http.StatusOK, map[string]any{
  140. "ok": true,
  141. })
  142. }
  143. func renderConversation(ctx *context.Context, comment *issues_model.Comment) {
  144. comments, err := issues_model.FetchCodeCommentsByLine(ctx, comment.Issue, ctx.Doer, comment.TreePath, comment.Line, ctx.Data["ShowOutdatedComments"].(bool))
  145. if err != nil {
  146. ctx.ServerError("FetchCodeCommentsByLine", err)
  147. return
  148. }
  149. ctx.Data["PageIsPullFiles"] = true
  150. ctx.Data["comments"] = comments
  151. ctx.Data["CanMarkConversation"] = true
  152. ctx.Data["Issue"] = comment.Issue
  153. if err = comment.Issue.LoadPullRequest(ctx); err != nil {
  154. ctx.ServerError("comment.Issue.LoadPullRequest", err)
  155. return
  156. }
  157. pullHeadCommitID, err := ctx.Repo.GitRepo.GetRefCommitID(comment.Issue.PullRequest.GetGitRefName())
  158. if err != nil {
  159. ctx.ServerError("GetRefCommitID", err)
  160. return
  161. }
  162. ctx.Data["AfterCommitID"] = pullHeadCommitID
  163. ctx.HTML(http.StatusOK, tplConversation)
  164. }
  165. // SubmitReview creates a review out of the existing pending review or creates a new one if no pending review exist
  166. func SubmitReview(ctx *context.Context) {
  167. form := web.GetForm(ctx).(*forms.SubmitReviewForm)
  168. issue := GetActionIssue(ctx)
  169. if ctx.Written() {
  170. return
  171. }
  172. if !issue.IsPull {
  173. return
  174. }
  175. if ctx.HasError() {
  176. ctx.Flash.Error(ctx.Data["ErrorMsg"].(string))
  177. ctx.Redirect(fmt.Sprintf("%s/pulls/%d/files", ctx.Repo.RepoLink, issue.Index))
  178. return
  179. }
  180. reviewType := form.ReviewType()
  181. switch reviewType {
  182. case issues_model.ReviewTypeUnknown:
  183. ctx.ServerError("ReviewType", fmt.Errorf("unknown ReviewType: %s", form.Type))
  184. return
  185. // can not approve/reject your own PR
  186. case issues_model.ReviewTypeApprove, issues_model.ReviewTypeReject:
  187. if issue.IsPoster(ctx.Doer.ID) {
  188. var translated string
  189. if reviewType == issues_model.ReviewTypeApprove {
  190. translated = ctx.Tr("repo.issues.review.self.approval")
  191. } else {
  192. translated = ctx.Tr("repo.issues.review.self.rejection")
  193. }
  194. ctx.Flash.Error(translated)
  195. ctx.Redirect(fmt.Sprintf("%s/pulls/%d/files", ctx.Repo.RepoLink, issue.Index))
  196. return
  197. }
  198. }
  199. var attachments []string
  200. if setting.Attachment.Enabled {
  201. attachments = form.Files
  202. }
  203. _, comm, err := pull_service.SubmitReview(ctx, ctx.Doer, ctx.Repo.GitRepo, issue, reviewType, form.Content, form.CommitID, attachments)
  204. if err != nil {
  205. if issues_model.IsContentEmptyErr(err) {
  206. ctx.Flash.Error(ctx.Tr("repo.issues.review.content.empty"))
  207. ctx.Redirect(fmt.Sprintf("%s/pulls/%d/files", ctx.Repo.RepoLink, issue.Index))
  208. } else {
  209. ctx.ServerError("SubmitReview", err)
  210. }
  211. return
  212. }
  213. ctx.Redirect(fmt.Sprintf("%s/pulls/%d#%s", ctx.Repo.RepoLink, issue.Index, comm.HashTag()))
  214. }
  215. // DismissReview dismissing stale review by repo admin
  216. func DismissReview(ctx *context.Context) {
  217. form := web.GetForm(ctx).(*forms.DismissReviewForm)
  218. comm, err := pull_service.DismissReview(ctx, form.ReviewID, ctx.Repo.Repository.ID, form.Message, ctx.Doer, true, true)
  219. if err != nil {
  220. ctx.ServerError("pull_service.DismissReview", err)
  221. return
  222. }
  223. ctx.Redirect(fmt.Sprintf("%s/pulls/%d#%s", ctx.Repo.RepoLink, comm.Issue.Index, comm.HashTag()))
  224. }
  225. // viewedFilesUpdate Struct to parse the body of a request to update the reviewed files of a PR
  226. // If you want to implement an API to update the review, simply move this struct into modules.
  227. type viewedFilesUpdate struct {
  228. Files map[string]bool `json:"files"`
  229. HeadCommitSHA string `json:"headCommitSHA"`
  230. }
  231. func UpdateViewedFiles(ctx *context.Context) {
  232. // Find corresponding PR
  233. issue := checkPullInfo(ctx)
  234. if ctx.Written() {
  235. return
  236. }
  237. pull := issue.PullRequest
  238. var data *viewedFilesUpdate
  239. err := json.NewDecoder(ctx.Req.Body).Decode(&data)
  240. if err != nil {
  241. log.Warn("Attempted to update a review but could not parse request body: %v", err)
  242. ctx.Resp.WriteHeader(http.StatusBadRequest)
  243. return
  244. }
  245. // Expect the review to have been now if no head commit was supplied
  246. if data.HeadCommitSHA == "" {
  247. data.HeadCommitSHA = pull.HeadCommitID
  248. }
  249. updatedFiles := make(map[string]pull_model.ViewedState, len(data.Files))
  250. for file, viewed := range data.Files {
  251. // Only unviewed and viewed are possible, has-changed can not be set from the outside
  252. state := pull_model.Unviewed
  253. if viewed {
  254. state = pull_model.Viewed
  255. }
  256. updatedFiles[file] = state
  257. }
  258. if err := pull_model.UpdateReviewState(ctx, ctx.Doer.ID, pull.ID, data.HeadCommitSHA, updatedFiles); err != nil {
  259. ctx.ServerError("UpdateReview", err)
  260. }
  261. }