2023-01-17 21:03:44 +00:00
|
|
|
// 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"
|
|
|
|
)
|
|
|
|
|
2024-02-16 12:16:11 +00:00
|
|
|
// 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)
|
|
|
|
}
|
2023-01-17 21:03:44 +00:00
|
|
|
|
2024-02-16 12:16:11 +00:00
|
|
|
tree[comment.TreePath][comment.Line] = append(tree[comment.TreePath][comment.Line], CodeConversation{comment})
|
2023-01-17 21:03:44 +00:00
|
|
|
}
|
|
|
|
|
2024-02-16 12:16:11 +00:00
|
|
|
// 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) {
|
2023-01-17 21:03:44 +00:00
|
|
|
pathToLineToComment := make(CodeComments)
|
|
|
|
if review == nil {
|
|
|
|
review = &Review{ID: 0}
|
|
|
|
}
|
|
|
|
opts := FindCommentsOptions{
|
|
|
|
Type: CommentTypeCode,
|
|
|
|
IssueID: issue.ID,
|
|
|
|
ReviewID: review.ID,
|
|
|
|
}
|
|
|
|
|
2024-02-16 12:16:11 +00:00
|
|
|
comments, err := findCodeComments(ctx, opts, issue, doer, review, showOutdatedComments)
|
2023-01-17 21:03:44 +00:00
|
|
|
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
|
|
|
|
}
|
|
|
|
|
2024-03-12 07:23:44 +00:00
|
|
|
func findCodeComments(ctx context.Context, opts FindCommentsOptions, issue *Issue, doer *user_model.User, review *Review, showOutdatedComments bool) (CommentList, error) {
|
2023-05-21 12:48:28 +00:00
|
|
|
var comments CommentList
|
2023-01-17 21:03:44 +00:00
|
|
|
if review == nil {
|
|
|
|
review = &Review{ID: 0}
|
|
|
|
}
|
|
|
|
conds := opts.ToConds()
|
2023-06-21 16:08:12 +00:00
|
|
|
|
|
|
|
if !showOutdatedComments && review.ID == 0 {
|
2023-01-17 21:03:44 +00:00
|
|
|
conds = conds.And(builder.Eq{"invalidated": false})
|
|
|
|
}
|
2023-06-21 16:08:12 +00:00
|
|
|
|
2023-01-17 21:03:44 +00:00
|
|
|
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
|
|
|
|
}
|
|
|
|
|
2023-05-21 12:48:28 +00:00
|
|
|
if err := comments.LoadPosters(ctx); err != nil {
|
2023-01-17 21:03:44 +00:00
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
|
2024-03-27 04:44:26 +00:00
|
|
|
if err := comments.LoadAttachments(ctx); err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
|
2023-01-17 21:03:44 +00:00
|
|
|
// 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 &&
|
2024-02-16 12:16:11 +00:00
|
|
|
(doer == nil || doer.ID != re.ReviewerID) {
|
2023-01-17 21:03:44 +00:00
|
|
|
continue
|
|
|
|
}
|
|
|
|
comment.Review = re
|
|
|
|
}
|
|
|
|
comments[n] = comment
|
|
|
|
n++
|
|
|
|
|
2023-09-29 12:12:54 +00:00
|
|
|
if err := comment.LoadResolveDoer(ctx); err != nil {
|
2023-01-17 21:03:44 +00:00
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
|
2023-09-29 12:12:54 +00:00
|
|
|
if err := comment.LoadReactions(ctx, issue.Repo); err != nil {
|
2023-01-17 21:03:44 +00:00
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
|
|
|
|
var err error
|
|
|
|
if comment.RenderedContent, err = markdown.RenderString(&markup.RenderContext{
|
2024-01-15 08:49:24 +00:00
|
|
|
Ctx: ctx,
|
|
|
|
Links: markup.Links{
|
|
|
|
Base: issue.Repo.Link(),
|
|
|
|
},
|
|
|
|
Metas: issue.Repo.ComposeMetas(ctx),
|
2023-01-17 21:03:44 +00:00
|
|
|
}, comment.Content); err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return comments[:n], nil
|
|
|
|
}
|
|
|
|
|
2024-02-16 12:16:11 +00:00
|
|
|
// FetchCodeConversation fetches the code conversation of a given comment (same review, treePath and line number)
|
2024-03-12 07:23:44 +00:00
|
|
|
func FetchCodeConversation(ctx context.Context, comment *Comment, doer *user_model.User) (CommentList, error) {
|
2023-01-17 21:03:44 +00:00
|
|
|
opts := FindCommentsOptions{
|
|
|
|
Type: CommentTypeCode,
|
2024-02-16 12:16:11 +00:00
|
|
|
IssueID: comment.IssueID,
|
|
|
|
ReviewID: comment.ReviewID,
|
|
|
|
TreePath: comment.TreePath,
|
|
|
|
Line: comment.Line,
|
2023-01-17 21:03:44 +00:00
|
|
|
}
|
2024-02-16 12:16:11 +00:00
|
|
|
return findCodeComments(ctx, opts, comment.Issue, doer, nil, true)
|
2023-01-17 21:03:44 +00:00
|
|
|
}
|