diff --git a/app/api/controller/pullreq/activity_list.go b/app/api/controller/pullreq/activity_list.go index 478809da1..40e0c090a 100644 --- a/app/api/controller/pullreq/activity_list.go +++ b/app/api/controller/pullreq/activity_list.go @@ -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 diff --git a/app/api/controller/pullreq/comment_create.go b/app/api/controller/pullreq/comment_create.go index fcd769aee..6fb087f8b 100644 --- a/app/api/controller/pullreq/comment_create.go +++ b/app/api/controller/pullreq/comment_create.go @@ -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 +} diff --git a/app/api/controller/pullreq/comment_update.go b/app/api/controller/pullreq/comment_update.go index 962577e2d..03f694457 100644 --- a/app/api/controller/pullreq/comment_update.go +++ b/app/api/controller/pullreq/comment_update.go @@ -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") } diff --git a/app/api/controller/pullreq/controller.go b/app/api/controller/pullreq/controller.go index a23211aa2..9d237fbf7 100644 --- a/app/api/controller/pullreq/controller.go +++ b/app/api/controller/pullreq/controller.go @@ -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, diff --git a/app/api/controller/pullreq/mentions.go b/app/api/controller/pullreq/mentions.go new file mode 100644 index 000000000..ed620650b --- /dev/null +++ b/app/api/controller/pullreq/mentions.go @@ -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 +} diff --git a/app/api/controller/pullreq/wire.go b/app/api/controller/pullreq/wire.go index 5019471be..eb2d34e52 100644 --- a/app/api/controller/pullreq/wire.go +++ b/app/api/controller/pullreq/wire.go @@ -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, diff --git a/app/services/notification/comment_created.go b/app/services/notification/comment_created.go index 00daa9853..19f2d37d6 100644 --- a/app/services/notification/comment_created.go +++ b/app/services/notification/comment_created.go @@ -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 -} diff --git a/cmd/gitness/wire_gen.go b/cmd/gitness/wire_gen.go index a9b687040..3b6996986 100644 --- a/cmd/gitness/wire_gen.go +++ b/cmd/gitness/wire_gen.go @@ -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) diff --git a/types/pullreq_activity.go b/types/pullreq_activity.go index 011211e18..8776b0608 100644 --- a/types/pullreq_activity.go +++ b/types/pullreq_activity.go @@ -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 { diff --git a/types/pullreq_activity_metadata.go b/types/pullreq_activity_metadata.go index dfa71e310..07c25a8c5 100644 --- a/types/pullreq_activity_metadata.go +++ b/types/pullreq_activity_metadata.go @@ -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 + } + }) +}