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.

review.go 8.9KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320
  1. // Copyright 2019 The Gitea Authors.
  2. // All rights reserved.
  3. // Use of this source code is governed by a MIT-style
  4. // license that can be found in the LICENSE file.
  5. package pull
  6. import (
  7. "fmt"
  8. "io"
  9. "regexp"
  10. "strings"
  11. "code.gitea.io/gitea/models"
  12. "code.gitea.io/gitea/modules/git"
  13. "code.gitea.io/gitea/modules/log"
  14. "code.gitea.io/gitea/modules/notification"
  15. "code.gitea.io/gitea/modules/setting"
  16. )
  17. // CreateCodeComment creates a comment on the code line
  18. func CreateCodeComment(doer *models.User, gitRepo *git.Repository, issue *models.Issue, line int64, content string, treePath string, isReview bool, replyReviewID int64, latestCommitID string) (*models.Comment, error) {
  19. var (
  20. existsReview bool
  21. err error
  22. )
  23. // CreateCodeComment() is used for:
  24. // - Single comments
  25. // - Comments that are part of a review
  26. // - Comments that reply to an existing review
  27. if !isReview && replyReviewID != 0 {
  28. // It's not part of a review; maybe a reply to a review comment or a single comment.
  29. // Check if there are reviews for that line already; if there are, this is a reply
  30. if existsReview, err = models.ReviewExists(issue, treePath, line); err != nil {
  31. return nil, err
  32. }
  33. }
  34. // Comments that are replies don't require a review header to show up in the issue view
  35. if !isReview && existsReview {
  36. if err = issue.LoadRepo(); err != nil {
  37. return nil, err
  38. }
  39. comment, err := createCodeComment(
  40. doer,
  41. issue.Repo,
  42. issue,
  43. content,
  44. treePath,
  45. line,
  46. replyReviewID,
  47. )
  48. if err != nil {
  49. return nil, err
  50. }
  51. mentions, err := issue.FindAndUpdateIssueMentions(models.DefaultDBContext(), doer, comment.Content)
  52. if err != nil {
  53. return nil, err
  54. }
  55. notification.NotifyCreateIssueComment(doer, issue.Repo, issue, comment, mentions)
  56. return comment, nil
  57. }
  58. review, err := models.GetCurrentReview(doer, issue)
  59. if err != nil {
  60. if !models.IsErrReviewNotExist(err) {
  61. return nil, err
  62. }
  63. if review, err = models.CreateReview(models.CreateReviewOptions{
  64. Type: models.ReviewTypePending,
  65. Reviewer: doer,
  66. Issue: issue,
  67. Official: false,
  68. CommitID: latestCommitID,
  69. }); err != nil {
  70. return nil, err
  71. }
  72. }
  73. comment, err := createCodeComment(
  74. doer,
  75. issue.Repo,
  76. issue,
  77. content,
  78. treePath,
  79. line,
  80. review.ID,
  81. )
  82. if err != nil {
  83. return nil, err
  84. }
  85. if !isReview && !existsReview {
  86. // Submit the review we've just created so the comment shows up in the issue view
  87. if _, _, err = SubmitReview(doer, gitRepo, issue, models.ReviewTypeComment, "", latestCommitID, nil); err != nil {
  88. return nil, err
  89. }
  90. }
  91. // NOTICE: if it's a pending review the notifications will not be fired until user submit review.
  92. return comment, nil
  93. }
  94. var notEnoughLines = regexp.MustCompile(`exit status 128 - fatal: file .* has only \d+ lines?`)
  95. // createCodeComment creates a plain code comment at the specified line / path
  96. func createCodeComment(doer *models.User, repo *models.Repository, issue *models.Issue, content, treePath string, line, reviewID int64) (*models.Comment, error) {
  97. var commitID, patch string
  98. if err := issue.LoadPullRequest(); err != nil {
  99. return nil, fmt.Errorf("GetPullRequestByIssueID: %v", err)
  100. }
  101. pr := issue.PullRequest
  102. if err := pr.LoadBaseRepo(); err != nil {
  103. return nil, fmt.Errorf("LoadHeadRepo: %v", err)
  104. }
  105. gitRepo, err := git.OpenRepository(pr.BaseRepo.RepoPath())
  106. if err != nil {
  107. return nil, fmt.Errorf("OpenRepository: %v", err)
  108. }
  109. defer gitRepo.Close()
  110. invalidated := false
  111. head := pr.GetGitRefName()
  112. if line > 0 {
  113. if reviewID != 0 {
  114. first, err := models.FindComments(&models.FindCommentsOptions{
  115. ReviewID: reviewID,
  116. Line: line,
  117. TreePath: treePath,
  118. Type: models.CommentTypeCode,
  119. ListOptions: models.ListOptions{
  120. PageSize: 1,
  121. Page: 1,
  122. },
  123. })
  124. if err == nil && len(first) > 0 {
  125. commitID = first[0].CommitSHA
  126. invalidated = first[0].Invalidated
  127. patch = first[0].Patch
  128. } else if err != nil && !models.IsErrCommentNotExist(err) {
  129. return nil, fmt.Errorf("Find first comment for %d line %d path %s. Error: %v", reviewID, line, treePath, err)
  130. } else {
  131. review, err := models.GetReviewByID(reviewID)
  132. if err == nil && len(review.CommitID) > 0 {
  133. head = review.CommitID
  134. } else if err != nil && !models.IsErrReviewNotExist(err) {
  135. return nil, fmt.Errorf("GetReviewByID %d. Error: %v", reviewID, err)
  136. }
  137. }
  138. }
  139. if len(commitID) == 0 {
  140. // FIXME validate treePath
  141. // Get latest commit referencing the commented line
  142. // No need for get commit for base branch changes
  143. commit, err := gitRepo.LineBlame(head, gitRepo.Path, treePath, uint(line))
  144. if err == nil {
  145. commitID = commit.ID.String()
  146. } else if !(strings.Contains(err.Error(), "exit status 128 - fatal: no such path") || notEnoughLines.MatchString(err.Error())) {
  147. return nil, fmt.Errorf("LineBlame[%s, %s, %s, %d]: %v", pr.GetGitRefName(), gitRepo.Path, treePath, line, err)
  148. }
  149. }
  150. }
  151. // Only fetch diff if comment is review comment
  152. if len(patch) == 0 && reviewID != 0 {
  153. headCommitID, err := gitRepo.GetRefCommitID(pr.GetGitRefName())
  154. if err != nil {
  155. return nil, fmt.Errorf("GetRefCommitID[%s]: %v", pr.GetGitRefName(), err)
  156. }
  157. if len(commitID) == 0 {
  158. commitID = headCommitID
  159. }
  160. reader, writer := io.Pipe()
  161. defer func() {
  162. _ = reader.Close()
  163. _ = writer.Close()
  164. }()
  165. go func() {
  166. if err := git.GetRepoRawDiffForFile(gitRepo, pr.MergeBase, headCommitID, git.RawDiffNormal, treePath, writer); err != nil {
  167. _ = writer.CloseWithError(fmt.Errorf("GetRawDiffForLine[%s, %s, %s, %s]: %v", gitRepo.Path, pr.MergeBase, headCommitID, treePath, err))
  168. return
  169. }
  170. _ = writer.Close()
  171. }()
  172. patch, err = git.CutDiffAroundLine(reader, int64((&models.Comment{Line: line}).UnsignedLine()), line < 0, setting.UI.CodeCommentLines)
  173. if err != nil {
  174. log.Error("Error whilst generating patch: %v", err)
  175. return nil, err
  176. }
  177. }
  178. return models.CreateComment(&models.CreateCommentOptions{
  179. Type: models.CommentTypeCode,
  180. Doer: doer,
  181. Repo: repo,
  182. Issue: issue,
  183. Content: content,
  184. LineNum: line,
  185. TreePath: treePath,
  186. CommitSHA: commitID,
  187. ReviewID: reviewID,
  188. Patch: patch,
  189. Invalidated: invalidated,
  190. })
  191. }
  192. // SubmitReview creates a review out of the existing pending review or creates a new one if no pending review exist
  193. func SubmitReview(doer *models.User, gitRepo *git.Repository, issue *models.Issue, reviewType models.ReviewType, content, commitID string, attachmentUUIDs []string) (*models.Review, *models.Comment, error) {
  194. pr, err := issue.GetPullRequest()
  195. if err != nil {
  196. return nil, nil, err
  197. }
  198. var stale bool
  199. if reviewType != models.ReviewTypeApprove && reviewType != models.ReviewTypeReject {
  200. stale = false
  201. } else {
  202. headCommitID, err := gitRepo.GetRefCommitID(pr.GetGitRefName())
  203. if err != nil {
  204. return nil, nil, err
  205. }
  206. if headCommitID == commitID {
  207. stale = false
  208. } else {
  209. stale, err = checkIfPRContentChanged(pr, commitID, headCommitID)
  210. if err != nil {
  211. return nil, nil, err
  212. }
  213. }
  214. }
  215. review, comm, err := models.SubmitReview(doer, issue, reviewType, content, commitID, stale, attachmentUUIDs)
  216. if err != nil {
  217. return nil, nil, err
  218. }
  219. ctx := models.DefaultDBContext()
  220. mentions, err := issue.FindAndUpdateIssueMentions(ctx, doer, comm.Content)
  221. if err != nil {
  222. return nil, nil, err
  223. }
  224. notification.NotifyPullRequestReview(pr, review, comm, mentions)
  225. for _, lines := range review.CodeComments {
  226. for _, comments := range lines {
  227. for _, codeComment := range comments {
  228. mentions, err := issue.FindAndUpdateIssueMentions(ctx, doer, codeComment.Content)
  229. if err != nil {
  230. return nil, nil, err
  231. }
  232. notification.NotifyPullRequestCodeComment(pr, codeComment, mentions)
  233. }
  234. }
  235. }
  236. return review, comm, nil
  237. }
  238. // DismissReview dismissing stale review by repo admin
  239. func DismissReview(reviewID int64, message string, doer *models.User, isDismiss bool) (comment *models.Comment, err error) {
  240. review, err := models.GetReviewByID(reviewID)
  241. if err != nil {
  242. return
  243. }
  244. if review.Type != models.ReviewTypeApprove && review.Type != models.ReviewTypeReject {
  245. return nil, fmt.Errorf("not need to dismiss this review because it's type is not Approve or change request")
  246. }
  247. if err = models.DismissReview(review, isDismiss); err != nil {
  248. return
  249. }
  250. if !isDismiss {
  251. return nil, nil
  252. }
  253. // load data for notify
  254. if err = review.LoadAttributes(); err != nil {
  255. return
  256. }
  257. if err = review.Issue.LoadPullRequest(); err != nil {
  258. return
  259. }
  260. if err = review.Issue.LoadAttributes(); err != nil {
  261. return
  262. }
  263. comment, err = models.CreateComment(&models.CreateCommentOptions{
  264. Doer: doer,
  265. Content: message,
  266. Type: models.CommentTypeDismissReview,
  267. ReviewID: review.ID,
  268. Issue: review.Issue,
  269. Repo: review.Issue.Repo,
  270. })
  271. if err != nil {
  272. return
  273. }
  274. comment.Review = review
  275. comment.Poster = doer
  276. comment.Issue = review.Issue
  277. notification.NotifyPullRevieweDismiss(doer, review, comment)
  278. return
  279. }