loading up the forgejo repo on tangled to test page performance
0
fork

Configure Feed

Select the types of activity you want to include in your feed.

at forgejo 465 lines 14 kB view raw
1// Copyright 2019 The Gitea Authors. 2// All rights reserved. 3// SPDX-License-Identifier: MIT 4 5package pull 6 7import ( 8 "context" 9 "fmt" 10 "io" 11 "regexp" 12 "strings" 13 14 "forgejo.org/models/db" 15 issues_model "forgejo.org/models/issues" 16 repo_model "forgejo.org/models/repo" 17 user_model "forgejo.org/models/user" 18 "forgejo.org/modules/git" 19 "forgejo.org/modules/gitrepo" 20 "forgejo.org/modules/log" 21 "forgejo.org/modules/optional" 22 "forgejo.org/modules/setting" 23 "forgejo.org/modules/util" 24 notify_service "forgejo.org/services/notify" 25) 26 27var notEnoughLines = regexp.MustCompile(`fatal: file .* has only \d+ lines?`) 28 29// ErrDismissRequestOnClosedPR represents an error when an user tries to dismiss a review associated to a closed or merged PR. 30type ErrDismissRequestOnClosedPR struct{} 31 32// IsErrDismissRequestOnClosedPR checks if an error is an ErrDismissRequestOnClosedPR. 33func IsErrDismissRequestOnClosedPR(err error) bool { 34 _, ok := err.(ErrDismissRequestOnClosedPR) 35 return ok 36} 37 38func (err ErrDismissRequestOnClosedPR) Error() string { 39 return "can't dismiss a review associated to a closed or merged PR" 40} 41 42func (err ErrDismissRequestOnClosedPR) Unwrap() error { 43 return util.ErrPermissionDenied 44} 45 46// checkInvalidation checks if the line of code comment got changed by another commit. 47// If the line got changed the comment is going to be invalidated. 48func checkInvalidation(ctx context.Context, c *issues_model.Comment, repo *git.Repository, branch string) error { 49 // FIXME differentiate between previous and proposed line 50 commit, err := repo.LineBlame(branch, repo.Path, c.TreePath, uint(c.UnsignedLine())) 51 if err != nil && (strings.Contains(err.Error(), "fatal: no such path") || notEnoughLines.MatchString(err.Error())) { 52 c.Invalidated = true 53 return issues_model.UpdateCommentInvalidate(ctx, c) 54 } 55 if err != nil { 56 return err 57 } 58 if c.CommitSHA != "" && c.CommitSHA != commit.ID.String() { 59 c.Invalidated = true 60 return issues_model.UpdateCommentInvalidate(ctx, c) 61 } 62 return nil 63} 64 65// InvalidateCodeComments will lookup the prs for code comments which got invalidated by change 66func InvalidateCodeComments(ctx context.Context, prs issues_model.PullRequestList, doer *user_model.User, repo *git.Repository, branch string) error { 67 if len(prs) == 0 { 68 return nil 69 } 70 issueIDs := prs.GetIssueIDs() 71 72 codeComments, err := db.Find[issues_model.Comment](ctx, issues_model.FindCommentsOptions{ 73 ListOptions: db.ListOptionsAll, 74 Type: issues_model.CommentTypeCode, 75 Invalidated: optional.Some(false), 76 IssueIDs: issueIDs, 77 }) 78 if err != nil { 79 return fmt.Errorf("find code comments: %v", err) 80 } 81 for _, comment := range codeComments { 82 if err := checkInvalidation(ctx, comment, repo, branch); err != nil { 83 return err 84 } 85 } 86 return nil 87} 88 89// CreateCodeComment creates a comment on the code line 90func CreateCodeComment(ctx context.Context, doer *user_model.User, gitRepo *git.Repository, issue *issues_model.Issue, line int64, content, treePath string, pendingReview bool, replyReviewID int64, latestCommitID string, attachments []string) (*issues_model.Comment, error) { 91 var ( 92 existsReview bool 93 err error 94 ) 95 96 // CreateCodeComment() is used for: 97 // - Single comments 98 // - Comments that are part of a review 99 // - Comments that reply to an existing review 100 101 if !pendingReview && replyReviewID != 0 { 102 // It's not part of a review; maybe a reply to a review comment or a single comment. 103 // Check if there are reviews for that line already; if there are, this is a reply 104 if existsReview, err = issues_model.ReviewExists(ctx, issue, treePath, line); err != nil { 105 return nil, err 106 } 107 } 108 109 // Comments that are replies don't require a review header to show up in the issue view 110 if !pendingReview && existsReview { 111 if err = issue.LoadRepo(ctx); err != nil { 112 return nil, err 113 } 114 115 comment, err := CreateCodeCommentKnownReviewID(ctx, 116 doer, 117 issue.Repo, 118 issue, 119 content, 120 treePath, 121 line, 122 replyReviewID, 123 attachments, 124 ) 125 if err != nil { 126 return nil, err 127 } 128 129 mentions, err := issues_model.FindAndUpdateIssueMentions(ctx, issue, doer, comment.Content) 130 if err != nil { 131 return nil, err 132 } 133 134 notify_service.CreateIssueComment(ctx, doer, issue.Repo, issue, comment, mentions) 135 136 return comment, nil 137 } 138 139 review, err := issues_model.GetCurrentReview(ctx, doer, issue) 140 if err != nil { 141 if !issues_model.IsErrReviewNotExist(err) { 142 return nil, err 143 } 144 145 if review, err = issues_model.CreateReview(ctx, issues_model.CreateReviewOptions{ 146 Type: issues_model.ReviewTypePending, 147 Reviewer: doer, 148 Issue: issue, 149 Official: false, 150 CommitID: latestCommitID, 151 }); err != nil { 152 return nil, err 153 } 154 } 155 156 comment, err := CreateCodeCommentKnownReviewID(ctx, 157 doer, 158 issue.Repo, 159 issue, 160 content, 161 treePath, 162 line, 163 review.ID, 164 attachments, 165 ) 166 if err != nil { 167 return nil, err 168 } 169 170 if !pendingReview && !existsReview { 171 // Submit the review we've just created so the comment shows up in the issue view 172 if _, _, err = SubmitReview(ctx, doer, gitRepo, issue, issues_model.ReviewTypeComment, "", latestCommitID, nil); err != nil { 173 return nil, err 174 } 175 } 176 177 // NOTICE: if it's a pending review the notifications will not be fired until user submit review. 178 179 return comment, nil 180} 181 182// CreateCodeCommentKnownReviewID creates a plain code comment at the specified line / path 183func CreateCodeCommentKnownReviewID(ctx context.Context, doer *user_model.User, repo *repo_model.Repository, issue *issues_model.Issue, content, treePath string, line, reviewID int64, attachments []string) (*issues_model.Comment, error) { 184 var commitID, patch string 185 if err := issue.LoadPullRequest(ctx); err != nil { 186 return nil, fmt.Errorf("LoadPullRequest: %w", err) 187 } 188 pr := issue.PullRequest 189 if err := pr.LoadBaseRepo(ctx); err != nil { 190 return nil, fmt.Errorf("LoadBaseRepo: %w", err) 191 } 192 gitRepo, closer, err := gitrepo.RepositoryFromContextOrOpen(ctx, pr.BaseRepo) 193 if err != nil { 194 return nil, fmt.Errorf("RepositoryFromContextOrOpen: %w", err) 195 } 196 defer closer.Close() 197 198 invalidated := false 199 head := pr.GetGitRefName() 200 if line > 0 { 201 if reviewID != 0 { 202 first, err := issues_model.FindComments(ctx, &issues_model.FindCommentsOptions{ 203 ReviewID: reviewID, 204 Line: line, 205 TreePath: treePath, 206 Type: issues_model.CommentTypeCode, 207 ListOptions: db.ListOptions{ 208 PageSize: 1, 209 Page: 1, 210 }, 211 }) 212 if err == nil && len(first) > 0 { 213 commitID = first[0].CommitSHA 214 invalidated = first[0].Invalidated 215 patch = first[0].Patch 216 } else if err != nil && !issues_model.IsErrCommentNotExist(err) { 217 return nil, fmt.Errorf("Find first comment for %d line %d path %s. Error: %w", reviewID, line, treePath, err) 218 } else { 219 review, err := issues_model.GetReviewByID(ctx, reviewID) 220 if err == nil && len(review.CommitID) > 0 { 221 head = review.CommitID 222 } else if err != nil && !issues_model.IsErrReviewNotExist(err) { 223 return nil, fmt.Errorf("GetReviewByID %d. Error: %w", reviewID, err) 224 } 225 } 226 } 227 228 if len(commitID) == 0 { 229 // FIXME validate treePath 230 // Get latest commit referencing the commented line 231 // No need for get commit for base branch changes 232 commit, err := gitRepo.LineBlame(head, gitRepo.Path, treePath, uint(line)) 233 if err == nil { 234 commitID = commit.ID.String() 235 } else if !strings.Contains(err.Error(), "exit status 128 - fatal: no such path") && !notEnoughLines.MatchString(err.Error()) { 236 return nil, fmt.Errorf("LineBlame[%s, %s, %s, %d]: %w", pr.GetGitRefName(), gitRepo.Path, treePath, line, err) 237 } 238 } 239 } 240 241 // Only fetch diff if comment is review comment 242 if len(patch) == 0 && reviewID != 0 { 243 headCommitID, err := gitRepo.GetRefCommitID(pr.GetGitRefName()) 244 if err != nil { 245 return nil, fmt.Errorf("GetRefCommitID[%s]: %w", pr.GetGitRefName(), err) 246 } 247 if len(commitID) == 0 { 248 commitID = headCommitID 249 } 250 reader, writer := io.Pipe() 251 defer func() { 252 _ = reader.Close() 253 _ = writer.Close() 254 }() 255 go func() { 256 if err := git.GetRepoRawDiffForFile(gitRepo, pr.MergeBase, headCommitID, git.RawDiffNormal, treePath, writer); err != nil { 257 _ = writer.CloseWithError(fmt.Errorf("GetRawDiffForLine[%s, %s, %s, %s]: %w", gitRepo.Path, pr.MergeBase, headCommitID, treePath, err)) 258 return 259 } 260 _ = writer.Close() 261 }() 262 263 patch, err = git.CutDiffAroundLine(reader, int64((&issues_model.Comment{Line: line}).UnsignedLine()), line < 0, setting.UI.CodeCommentLines) 264 if err != nil { 265 log.Error("Error whilst generating patch: %v", err) 266 return nil, err 267 } 268 } 269 return issues_model.CreateComment(ctx, &issues_model.CreateCommentOptions{ 270 Type: issues_model.CommentTypeCode, 271 Doer: doer, 272 Repo: repo, 273 Issue: issue, 274 Content: content, 275 LineNum: line, 276 TreePath: treePath, 277 CommitSHA: commitID, 278 ReviewID: reviewID, 279 Patch: patch, 280 Invalidated: invalidated, 281 Attachments: attachments, 282 }) 283} 284 285// SubmitReview creates a review out of the existing pending review or creates a new one if no pending review exist 286func SubmitReview(ctx context.Context, doer *user_model.User, gitRepo *git.Repository, issue *issues_model.Issue, reviewType issues_model.ReviewType, content, commitID string, attachmentUUIDs []string) (*issues_model.Review, *issues_model.Comment, error) { 287 if err := issue.LoadPullRequest(ctx); err != nil { 288 return nil, nil, err 289 } 290 291 pr := issue.PullRequest 292 var stale bool 293 if reviewType != issues_model.ReviewTypeApprove && reviewType != issues_model.ReviewTypeReject { 294 stale = false 295 } else { 296 headCommitID, err := gitRepo.GetRefCommitID(pr.GetGitRefName()) 297 if err != nil { 298 return nil, nil, err 299 } 300 301 if headCommitID == commitID { 302 stale = false 303 } else { 304 stale, err = checkIfPRContentChanged(ctx, pr, commitID, headCommitID) 305 if err != nil { 306 return nil, nil, err 307 } 308 } 309 } 310 311 review, comm, err := issues_model.SubmitReview(ctx, doer, issue, reviewType, content, commitID, stale, attachmentUUIDs) 312 if err != nil { 313 return nil, nil, err 314 } 315 316 mentions, err := issues_model.FindAndUpdateIssueMentions(ctx, issue, doer, comm.Content) 317 if err != nil { 318 return nil, nil, err 319 } 320 321 notify_service.PullRequestReview(ctx, pr, review, comm, mentions) 322 323 for _, lines := range review.CodeComments { 324 for _, comments := range lines { 325 for _, codeComment := range comments { 326 mentions, err := issues_model.FindAndUpdateIssueMentions(ctx, issue, doer, codeComment.Content) 327 if err != nil { 328 return nil, nil, err 329 } 330 notify_service.PullRequestCodeComment(ctx, pr, codeComment, mentions) 331 } 332 } 333 } 334 335 return review, comm, nil 336} 337 338// DismissApprovalReviews dismiss all approval reviews because of new commits 339func DismissApprovalReviews(ctx context.Context, doer *user_model.User, pull *issues_model.PullRequest) error { 340 reviews, err := issues_model.FindReviews(ctx, issues_model.FindReviewOptions{ 341 ListOptions: db.ListOptionsAll, 342 IssueID: pull.IssueID, 343 Types: []issues_model.ReviewType{issues_model.ReviewTypeApprove}, 344 Dismissed: optional.Some(false), 345 }) 346 if err != nil { 347 return err 348 } 349 350 if err := reviews.LoadIssues(ctx); err != nil { 351 return err 352 } 353 354 return db.WithTx(ctx, func(ctx context.Context) error { 355 for _, review := range reviews { 356 if err := issues_model.DismissReview(ctx, review, true); err != nil { 357 return err 358 } 359 360 comment, err := issues_model.CreateComment(ctx, &issues_model.CreateCommentOptions{ 361 Doer: doer, 362 Content: "New commits pushed, approval review dismissed automatically according to repository settings", 363 Type: issues_model.CommentTypeDismissReview, 364 ReviewID: review.ID, 365 Issue: review.Issue, 366 Repo: review.Issue.Repo, 367 }) 368 if err != nil { 369 return err 370 } 371 372 comment.Review = review 373 comment.Poster = doer 374 comment.Issue = review.Issue 375 376 notify_service.PullReviewDismiss(ctx, doer, review, comment) 377 } 378 return nil 379 }) 380} 381 382// DismissReview dismissing stale review by repo admin 383func DismissReview(ctx context.Context, reviewID, repoID int64, message string, doer *user_model.User, isDismiss, dismissPriors bool) (comment *issues_model.Comment, err error) { 384 review, err := issues_model.GetReviewByID(ctx, reviewID) 385 if err != nil { 386 return nil, err 387 } 388 389 if review.Type != issues_model.ReviewTypeApprove && review.Type != issues_model.ReviewTypeReject { 390 return nil, fmt.Errorf("not need to dismiss this review because it's type is not Approve or change request") 391 } 392 393 // load data for notify 394 if err := review.LoadAttributes(ctx); err != nil { 395 return nil, err 396 } 397 398 // Check if the review's repoID is the one we're currently expecting. 399 if review.Issue.RepoID != repoID { 400 return nil, fmt.Errorf("reviews's repository is not the same as the one we expect") 401 } 402 403 issue := review.Issue 404 405 if issue.IsClosed { 406 return nil, ErrDismissRequestOnClosedPR{} 407 } 408 409 if issue.IsPull { 410 if err := issue.LoadPullRequest(ctx); err != nil { 411 return nil, err 412 } 413 if issue.PullRequest.HasMerged { 414 return nil, ErrDismissRequestOnClosedPR{} 415 } 416 } 417 418 if err := issues_model.DismissReview(ctx, review, isDismiss); err != nil { 419 return nil, err 420 } 421 422 if dismissPriors { 423 reviews, err := issues_model.FindReviews(ctx, issues_model.FindReviewOptions{ 424 IssueID: review.IssueID, 425 ReviewerID: review.ReviewerID, 426 Dismissed: optional.Some(false), 427 }) 428 if err != nil { 429 return nil, err 430 } 431 for _, oldReview := range reviews { 432 if err = issues_model.DismissReview(ctx, oldReview, true); err != nil { 433 return nil, err 434 } 435 } 436 } 437 438 if !isDismiss { 439 return nil, nil 440 } 441 442 if err := review.Issue.LoadAttributes(ctx); err != nil { 443 return nil, err 444 } 445 446 comment, err = issues_model.CreateComment(ctx, &issues_model.CreateCommentOptions{ 447 Doer: doer, 448 Content: message, 449 Type: issues_model.CommentTypeDismissReview, 450 ReviewID: review.ID, 451 Issue: review.Issue, 452 Repo: review.Issue.Repo, 453 }) 454 if err != nil { 455 return nil, err 456 } 457 458 comment.Review = review 459 comment.Poster = doer 460 comment.Issue = review.Issue 461 462 notify_service.PullReviewDismiss(ctx, doer, review, comment) 463 464 return comment, nil 465}