Skip to content

Commit

Permalink
Add comment mentions metadata and return id to principal info mapping…
Browse files Browse the repository at this point in the history
… in list activity response (#2024)
  • Loading branch information
darkodraskovic authored and Harness committed May 10, 2024
1 parent 423801d commit e31f33a
Show file tree
Hide file tree
Showing 10 changed files with 167 additions and 44 deletions.
10 changes: 10 additions & 0 deletions app/api/controller/pullreq/activity_list.go
Expand Up @@ -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
Expand Down
35 changes: 35 additions & 0 deletions app/api/controller/pullreq/comment_create.go
Expand Up @@ -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)
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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
}
10 changes: 10 additions & 0 deletions app/api/controller/pullreq/comment_update.go
Expand Up @@ -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)
Expand All @@ -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")
}
Expand Down
3 changes: 3 additions & 0 deletions app/api/controller/pullreq/controller.go
Expand Up @@ -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
Expand All @@ -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,
Expand All @@ -97,6 +99,7 @@ func NewController(
reviewerStore: pullreqReviewerStore,
repoStore: repoStore,
principalStore: principalStore,
principalInfoCache: principalInfoCache,
fileViewStore: fileViewStore,
membershipStore: membershipStore,
checkStore: checkStore,
Expand Down
63 changes: 63 additions & 0 deletions 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
}
4 changes: 2 additions & 2 deletions app/api/controller/pullreq/wire.go
Expand Up @@ -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,
Expand All @@ -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,
Expand Down
56 changes: 15 additions & 41 deletions app/services/notification/comment_created.go
Expand Up @@ -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 {
Expand Down Expand Up @@ -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
}
Expand All @@ -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
Expand Down Expand Up @@ -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
}
2 changes: 1 addition & 1 deletion cmd/gitness/wire_gen.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 2 additions & 0 deletions types/pullreq_activity.go
Expand Up @@ -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 {
Expand Down
26 changes: 26 additions & 0 deletions types/pullreq_activity_metadata.go
Expand Up @@ -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 {
Expand Down Expand Up @@ -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
}
})
}

0 comments on commit e31f33a

Please sign in to comment.