Add comment mentions metadata and return id to principal info mapping in list activity response (#2024)

VivekHarness-patch-1
Darko Draskovic 2024-05-10 23:22:59 +00:00 committed by Harness
parent 423801d51a
commit e31f33adde
10 changed files with 167 additions and 44 deletions

View File

@ -47,6 +47,16 @@ func (c *Controller) ActivityList(
return nil, fmt.Errorf("failed to list pull requests activities: %w", err)
}
for _, act := range list {
if act.Metadata != nil && act.Metadata.Mentions != nil {
mentions, err := c.principalInfoCache.Map(ctx, act.Metadata.Mentions.IDs)
if err != nil {
return nil, fmt.Errorf("failed to fetch activity mentions from principalInfoView: %w", err)
}
act.Mentions = mentions
}
}
list = removeDeletedComments(list)
return list, nil

View File

@ -127,6 +127,12 @@ func (c *Controller) CommentCreate(
// generate all metadata updates
var metadataUpdates []types.PullReqActivityMetadataUpdate
metadataUpdates, principalInfos, err := c.appendMetadataUpdateForMentions(
ctx, metadataUpdates, in.Text)
if err != nil {
return nil, fmt.Errorf("failed to update metadata for mentions: %w", err)
}
// suggestion metadata in case of code comments or code comment replies (don't restrict to either side for now).
if in.IsCodeComment() || (in.IsReply() && parentAct.IsValidCodeComment()) {
metadataUpdates = appendMetadataUpdateForSuggestions(metadataUpdates, in.Text)
@ -192,6 +198,9 @@ func (c *Controller) CommentCreate(
return nil, err
}
// Populate activity mentions (used only for response purposes).
act.Mentions = principalInfos
if in.IsCodeComment() {
// Migrate the comment if necessary... Note: we still need to return the code comment as is.
c.migrateCodeComment(ctx, repo, pr, in, act.AsCodeComment(), cut)
@ -428,3 +437,29 @@ func appendMetadataUpdateForSuggestions(
}),
)
}
func (c *Controller) appendMetadataUpdateForMentions(
ctx context.Context,
updates []types.PullReqActivityMetadataUpdate,
comment string,
) ([]types.PullReqActivityMetadataUpdate, map[int64]*types.PrincipalInfo, error) {
principalInfos, err := c.processMentions(ctx, comment)
if err != nil {
return nil, map[int64]*types.PrincipalInfo{}, err
}
ids := make([]int64, len(principalInfos))
i := 0
for id := range principalInfos {
ids[i] = id
i++
}
return append(
updates,
types.WithPullReqActivityMentionsMetadataUpdate(
func(m *types.PullReqActivityMentionsMetadata) {
m.IDs = ids
}),
), principalInfos, nil
}

View File

@ -83,6 +83,13 @@ func (c *Controller) CommentUpdate(
// generate all metadata updates
var metadataUpdates []types.PullReqActivityMetadataUpdate
metadataUpdates, principalInfos, err := c.appendMetadataUpdateForMentions(
ctx, metadataUpdates, in.Text,
)
if err != nil {
return nil, fmt.Errorf("failed to update metadata for mentions: %w", err)
}
// suggestion metadata in case of code comments or code comment replies (don't restrict to either side for now).
if act.IsValidCodeComment() || (act.IsReply() && parentAct.IsValidCodeComment()) {
metadataUpdates = appendMetadataUpdateForSuggestions(metadataUpdates, in.Text)
@ -99,6 +106,9 @@ func (c *Controller) CommentUpdate(
return nil, fmt.Errorf("failed to update comment: %w", err)
}
// Populate activity mentions (used only for response purposes).
act.Mentions = principalInfos
if err = c.sseStreamer.Publish(ctx, repo.ParentID, enum.SSETypePullRequestUpdated, pr); err != nil {
log.Ctx(ctx).Warn().Err(err).Msg("failed to publish PR changed event")
}

View File

@ -50,6 +50,7 @@ type Controller struct {
reviewerStore store.PullReqReviewerStore
repoStore store.RepoStore
principalStore store.PrincipalStore
principalInfoCache store.PrincipalInfoCache
fileViewStore store.PullReqFileViewStore
membershipStore store.MembershipStore
checkStore store.CheckStore
@ -74,6 +75,7 @@ func NewController(
pullreqReviewerStore store.PullReqReviewerStore,
repoStore store.RepoStore,
principalStore store.PrincipalStore,
principalInfoCache store.PrincipalInfoCache,
fileViewStore store.PullReqFileViewStore,
membershipStore store.MembershipStore,
checkStore store.CheckStore,
@ -97,6 +99,7 @@ func NewController(
reviewerStore: pullreqReviewerStore,
repoStore: repoStore,
principalStore: principalStore,
principalInfoCache: principalInfoCache,
fileViewStore: fileViewStore,
membershipStore: membershipStore,
checkStore: checkStore,

View File

@ -0,0 +1,63 @@
// Copyright 2023 Harness, Inc.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package pullreq
import (
"context"
"fmt"
"regexp"
"strconv"
"github.com/harness/gitness/types"
"github.com/rs/zerolog/log"
)
func (c *Controller) processMentions(
ctx context.Context,
text string,
) (map[int64]*types.PrincipalInfo, error) {
mentions := parseMentions(ctx, text)
if len(mentions) == 0 {
return map[int64]*types.PrincipalInfo{}, nil
}
infos, err := c.principalInfoCache.Map(ctx, mentions)
if err != nil {
return nil, fmt.Errorf("failed to fetch info from principalInfoCache: %w", err)
}
return infos, nil
}
var mentionRegex = regexp.MustCompile(`@\[(\d+)\]`)
func parseMentions(ctx context.Context, text string) []int64 {
matches := mentionRegex.FindAllStringSubmatch(text, -1)
var mentions []int64
for _, match := range matches {
if len(match) < 2 {
continue
}
if mention, err := strconv.ParseInt(match[1], 10, 64); err == nil {
mentions = append(mentions, mention)
} else {
log.Ctx(ctx).Warn().Err(err).Msgf("failed to parse mention %q", match[1])
}
}
return mentions
}

View File

@ -40,7 +40,7 @@ func ProvideController(tx dbtx.Transactor, urlProvider url.Provider, authorizer
pullReqStore store.PullReqStore, pullReqActivityStore store.PullReqActivityStore,
codeCommentsView store.CodeCommentView,
pullReqReviewStore store.PullReqReviewStore, pullReqReviewerStore store.PullReqReviewerStore,
repoStore store.RepoStore, principalStore store.PrincipalStore,
repoStore store.RepoStore, principalStore store.PrincipalStore, principalInfoCache store.PrincipalInfoCache,
fileViewStore store.PullReqFileViewStore, membershipStore store.MembershipStore,
checkStore store.CheckStore,
rpcClient git.Interface, eventReporter *pullreqevents.Reporter, codeCommentMigrator *codecomments.Migrator,
@ -51,7 +51,7 @@ func ProvideController(tx dbtx.Transactor, urlProvider url.Provider, authorizer
pullReqStore, pullReqActivityStore,
codeCommentsView,
pullReqReviewStore, pullReqReviewerStore,
repoStore, principalStore,
repoStore, principalStore, principalInfoCache,
fileViewStore, membershipStore,
checkStore,
rpcClient, eventReporter,

View File

@ -17,14 +17,10 @@ package notification
import (
"context"
"fmt"
"regexp"
"strconv"
pullreqevents "github.com/harness/gitness/app/events/pullreq"
"github.com/harness/gitness/events"
"github.com/harness/gitness/types"
"github.com/rs/zerolog/log"
)
type CommentPayload struct {
@ -125,7 +121,7 @@ func (s *Service) processCommentCreatedEvent(
seen[commenter.ID] = true
// process mentions
mentions, err = s.processMentions(ctx, activity.Text, seen)
mentions, err = s.processMentions(ctx, activity.Metadata, seen)
if err != nil {
return nil, nil, nil, nil, err
}
@ -147,29 +143,27 @@ func (s *Service) processCommentCreatedEvent(
func (s *Service) processMentions(
ctx context.Context,
text string,
metadata *types.PullReqActivityMetadata,
seen map[int64]bool,
) ([]*types.PrincipalInfo, error) {
var mentions []*types.PrincipalInfo
commentMentions := parseMentions(ctx, text)
if len(commentMentions) == 0 {
if metadata == nil || metadata.Mentions == nil {
return []*types.PrincipalInfo{}, nil
}
var mentionIDs []int64
for _, mentionID := range commentMentions {
if !seen[mentionID] {
mentionIDs = append(mentionIDs, mentionID)
seen[mentionID] = true
var ids []int64
for _, id := range metadata.Mentions.IDs {
if !seen[id] {
ids = append(ids, id)
seen[id] = true
}
}
if len(mentionIDs) > 0 {
var err error
mentions, err = s.principalInfoView.FindMany(ctx, mentionIDs)
if err != nil {
return nil, fmt.Errorf("failed to fetch thread mentions from principalInfoView: %w", err)
}
if len(ids) == 0 {
return []*types.PrincipalInfo{}, nil
}
mentions, err := s.principalInfoView.FindMany(ctx, ids)
if err != nil {
return nil, fmt.Errorf("failed to fetch thread mentions from principalInfoView: %w", err)
}
return mentions, nil
@ -213,23 +207,3 @@ func (s *Service) processParticipants(
return participants, nil
}
var mentionRegex = regexp.MustCompile(`@\[(\d+)\]`)
func parseMentions(ctx context.Context, text string) []int64 {
matches := mentionRegex.FindAllStringSubmatch(text, -1)
var mentions []int64
for _, match := range matches {
if len(match) < 2 {
continue
}
if mention, err := strconv.ParseInt(match[1], 10, 64); err == nil {
mentions = append(mentions, mention)
} else {
log.Ctx(ctx).Warn().Err(err).Msgf("failed to parse mention %q", match[1])
}
}
return mentions
}

View File

@ -257,7 +257,7 @@ func initSystem(ctx context.Context, config *types.Config) (*server.System, erro
if err != nil {
return nil, err
}
pullreqController := pullreq2.ProvideController(transactor, provider, authorizer, pullReqStore, pullReqActivityStore, codeCommentView, pullReqReviewStore, pullReqReviewerStore, repoStore, principalStore, pullReqFileViewStore, membershipStore, checkStore, gitInterface, eventsReporter, migrator, pullreqService, protectionManager, streamer, codeownersService, lockerLocker)
pullreqController := pullreq2.ProvideController(transactor, provider, authorizer, pullReqStore, pullReqActivityStore, codeCommentView, pullReqReviewStore, pullReqReviewerStore, repoStore, principalStore, principalInfoCache, pullReqFileViewStore, membershipStore, checkStore, gitInterface, eventsReporter, migrator, pullreqService, protectionManager, streamer, codeownersService, lockerLocker)
webhookConfig := server.ProvideWebhookConfig(config)
webhookStore := database.ProvideWebhookStore(db)
webhookExecutionStore := database.ProvideWebhookExecutionStore(db)

View File

@ -55,6 +55,8 @@ type PullReqActivity struct {
Resolver *PrincipalInfo `json:"resolver,omitempty"`
CodeComment *CodeCommentFields `json:"code_comment,omitempty"`
Mentions map[int64]*PrincipalInfo `json:"mentions,omitempty"` // used only in response
}
func (a *PullReqActivity) IsValidCodeComment() bool {

View File

@ -17,6 +17,7 @@ package types
// PullReqActivityMetadata contains metadata related to pull request activity.
type PullReqActivityMetadata struct {
Suggestions *PullReqActivitySuggestionsMetadata `json:"suggestions,omitempty"`
Mentions *PullReqActivityMentionsMetadata `json:"mentions,omitempty"`
}
func (m *PullReqActivityMetadata) IsEmpty() bool {
@ -64,3 +65,28 @@ func WithPullReqActivitySuggestionsMetadataUpdate(
}
})
}
// PullReqActivityMentionsMetadata contains metadata for code comment mentions.
type PullReqActivityMentionsMetadata struct {
IDs []int64 `json:"ids,omitempty"`
}
func (m *PullReqActivityMentionsMetadata) IsEmpty() bool {
return len(m.IDs) == 0
}
func WithPullReqActivityMentionsMetadataUpdate(
f func(m *PullReqActivityMentionsMetadata),
) PullReqActivityMetadataUpdate {
return pullReqActivityMetadataUpdateFunc(func(m *PullReqActivityMetadata) {
if m.Mentions == nil {
m.Mentions = &PullReqActivityMentionsMetadata{}
}
f(m.Mentions)
if m.Mentions.IsEmpty() {
m.Mentions = nil
}
})
}