forgejo/models/issues/comment_code.go
Lunny Xiao c9854bee98 Do some performance optimize for issues list and view issue/pull (gitea#29515)
This PR do some performance optimzations.

- [x] Add `index` for the column `comment_id` of `Attachment` table to
accelerate query from the database.
- [x] Remove unnecessary database queries when viewing issues. Before
some conditions which id = 0 will be sent to the database
- [x] Remove duplicated load posters
- [x] Batch loading attachements, isread of comments on viewing issue

---------

Co-authored-by: Zettat123 <zettat123@gmail.com>
Conflicts:
models/issues/comment_code.go: function was renamed in Forgejo
models/migrations/migrations.go: migration already ported
2024-04-08 14:47:31 +02:00

182 lines
5.4 KiB
Go

// Copyright 2022 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package issues
import (
"context"
"code.gitea.io/gitea/models/db"
user_model "code.gitea.io/gitea/models/user"
"code.gitea.io/gitea/modules/markup"
"code.gitea.io/gitea/modules/markup/markdown"
"xorm.io/builder"
)
// CodeConversation contains the comment of a given review
type CodeConversation []*Comment
// CodeConversationsAtLine contains the conversations for a given line
type CodeConversationsAtLine map[int64][]CodeConversation
// CodeConversationsAtLineAndTreePath contains the conversations for a given TreePath and line
type CodeConversationsAtLineAndTreePath map[string]CodeConversationsAtLine
func newCodeConversationsAtLineAndTreePath(comments []*Comment) CodeConversationsAtLineAndTreePath {
tree := make(CodeConversationsAtLineAndTreePath)
for _, comment := range comments {
tree.insertComment(comment)
}
return tree
}
func (tree CodeConversationsAtLineAndTreePath) insertComment(comment *Comment) {
// attempt to append comment to existing conversations (i.e. list of comments belonging to the same review)
for i, conversation := range tree[comment.TreePath][comment.Line] {
if conversation[0].ReviewID == comment.ReviewID {
tree[comment.TreePath][comment.Line][i] = append(conversation, comment)
return
}
}
// no previous conversation was found at this line, create it
if tree[comment.TreePath] == nil {
tree[comment.TreePath] = make(map[int64][]CodeConversation)
}
tree[comment.TreePath][comment.Line] = append(tree[comment.TreePath][comment.Line], CodeConversation{comment})
}
// FetchCodeConversations will return a 2d-map: ["Path"]["Line"] = List of CodeConversation (one per review) for this line
func FetchCodeConversations(ctx context.Context, issue *Issue, doer *user_model.User, showOutdatedComments bool) (CodeConversationsAtLineAndTreePath, error) {
opts := FindCommentsOptions{
Type: CommentTypeCode,
IssueID: issue.ID,
}
comments, err := findCodeComments(ctx, opts, issue, doer, nil, showOutdatedComments)
if err != nil {
return nil, err
}
return newCodeConversationsAtLineAndTreePath(comments), nil
}
// CodeComments represents comments on code by using this structure: FILENAME -> LINE (+ == proposed; - == previous) -> COMMENTS
type CodeComments map[string]map[int64][]*Comment
func fetchCodeCommentsByReview(ctx context.Context, issue *Issue, doer *user_model.User, review *Review, showOutdatedComments bool) (CodeComments, error) {
pathToLineToComment := make(CodeComments)
if review == nil {
review = &Review{ID: 0}
}
opts := FindCommentsOptions{
Type: CommentTypeCode,
IssueID: issue.ID,
ReviewID: review.ID,
}
comments, err := findCodeComments(ctx, opts, issue, doer, review, showOutdatedComments)
if err != nil {
return nil, err
}
for _, comment := range comments {
if pathToLineToComment[comment.TreePath] == nil {
pathToLineToComment[comment.TreePath] = make(map[int64][]*Comment)
}
pathToLineToComment[comment.TreePath][comment.Line] = append(pathToLineToComment[comment.TreePath][comment.Line], comment)
}
return pathToLineToComment, nil
}
func findCodeComments(ctx context.Context, opts FindCommentsOptions, issue *Issue, doer *user_model.User, review *Review, showOutdatedComments bool) (CommentList, error) {
var comments CommentList
if review == nil {
review = &Review{ID: 0}
}
conds := opts.ToConds()
if !showOutdatedComments && review.ID == 0 {
conds = conds.And(builder.Eq{"invalidated": false})
}
e := db.GetEngine(ctx)
if err := e.Where(conds).
Asc("comment.created_unix").
Asc("comment.id").
Find(&comments); err != nil {
return nil, err
}
if err := issue.LoadRepo(ctx); err != nil {
return nil, err
}
if err := comments.LoadPosters(ctx); err != nil {
return nil, err
}
if err := comments.LoadAttachments(ctx); err != nil {
return nil, err
}
// Find all reviews by ReviewID
reviews := make(map[int64]*Review)
ids := make([]int64, 0, len(comments))
for _, comment := range comments {
if comment.ReviewID != 0 {
ids = append(ids, comment.ReviewID)
}
}
if err := e.In("id", ids).Find(&reviews); err != nil {
return nil, err
}
n := 0
for _, comment := range comments {
if re, ok := reviews[comment.ReviewID]; ok && re != nil {
// If the review is pending only the author can see the comments (except if the review is set)
if review.ID == 0 && re.Type == ReviewTypePending &&
(doer == nil || doer.ID != re.ReviewerID) {
continue
}
comment.Review = re
}
comments[n] = comment
n++
if err := comment.LoadResolveDoer(ctx); err != nil {
return nil, err
}
if err := comment.LoadReactions(ctx, issue.Repo); err != nil {
return nil, err
}
var err error
if comment.RenderedContent, err = markdown.RenderString(&markup.RenderContext{
Ctx: ctx,
Links: markup.Links{
Base: issue.Repo.Link(),
},
Metas: issue.Repo.ComposeMetas(ctx),
}, comment.Content); err != nil {
return nil, err
}
}
return comments[:n], nil
}
// FetchCodeConversation fetches the code conversation of a given comment (same review, treePath and line number)
func FetchCodeConversation(ctx context.Context, comment *Comment, doer *user_model.User) (CommentList, error) {
opts := FindCommentsOptions{
Type: CommentTypeCode,
IssueID: comment.IssueID,
ReviewID: comment.ReviewID,
TreePath: comment.TreePath,
Line: comment.Line,
}
return findCodeComments(ctx, opts, comment.Issue, doer, nil, true)
}