diff options
Diffstat (limited to 'models/issues/comment_code.go')
-rw-r--r-- | models/issues/comment_code.go | 181 |
1 files changed, 181 insertions, 0 deletions
diff --git a/models/issues/comment_code.go b/models/issues/comment_code.go new file mode 100644 index 00000000..2f6f57e0 --- /dev/null +++ b/models/issues/comment_code.go @@ -0,0 +1,181 @@ +// 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) +} |