mirror of https://github.com/harness/drone.git
843 lines
26 KiB
Go
843 lines
26 KiB
Go
// 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 migrate
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"fmt"
|
|
"sort"
|
|
"strings"
|
|
"time"
|
|
|
|
"github.com/harness/gitness/app/store"
|
|
"github.com/harness/gitness/app/url"
|
|
"github.com/harness/gitness/errors"
|
|
"github.com/harness/gitness/git"
|
|
"github.com/harness/gitness/git/parser"
|
|
"github.com/harness/gitness/lock"
|
|
gitness_store "github.com/harness/gitness/store"
|
|
"github.com/harness/gitness/store/database/dbtx"
|
|
"github.com/harness/gitness/types"
|
|
"github.com/harness/gitness/types/enum"
|
|
|
|
"github.com/rs/zerolog/log"
|
|
)
|
|
|
|
// PullReq is pull request migrate.
|
|
type PullReq struct {
|
|
urlProvider url.Provider
|
|
git git.Interface
|
|
principalStore store.PrincipalStore
|
|
spaceStore store.SpaceStore
|
|
repoStore store.RepoStore
|
|
pullReqStore store.PullReqStore
|
|
pullReqActStore store.PullReqActivityStore
|
|
labelStore store.LabelStore
|
|
labelValueStore store.LabelValueStore
|
|
pullReqLabelAssignmentStore store.PullReqLabelAssignmentStore
|
|
tx dbtx.Transactor
|
|
mtxManager lock.MutexManager
|
|
}
|
|
|
|
func NewPullReq(
|
|
urlProvider url.Provider,
|
|
git git.Interface,
|
|
principalStore store.PrincipalStore,
|
|
spaceStore store.SpaceStore,
|
|
repoStore store.RepoStore,
|
|
pullReqStore store.PullReqStore,
|
|
pullReqActStore store.PullReqActivityStore,
|
|
labelStore store.LabelStore,
|
|
labelValueStore store.LabelValueStore,
|
|
pullReqLabelAssignmentStore store.PullReqLabelAssignmentStore,
|
|
tx dbtx.Transactor,
|
|
mtxManager lock.MutexManager,
|
|
) *PullReq {
|
|
return &PullReq{
|
|
urlProvider: urlProvider,
|
|
git: git,
|
|
principalStore: principalStore,
|
|
spaceStore: spaceStore,
|
|
repoStore: repoStore,
|
|
pullReqStore: pullReqStore,
|
|
pullReqActStore: pullReqActStore,
|
|
labelStore: labelStore,
|
|
labelValueStore: labelValueStore,
|
|
pullReqLabelAssignmentStore: pullReqLabelAssignmentStore,
|
|
tx: tx,
|
|
mtxManager: mtxManager,
|
|
}
|
|
}
|
|
|
|
type repoImportState struct {
|
|
git git.Interface
|
|
readParams git.ReadParams
|
|
principalStore store.PrincipalStore
|
|
spaceStore store.SpaceStore
|
|
pullReqActivityStore store.PullReqActivityStore
|
|
labelStore store.LabelStore
|
|
labelValueStore store.LabelValueStore
|
|
pullReqLabelAssignmentStore store.PullReqLabelAssignmentStore
|
|
branchCheck map[string]*git.Branch
|
|
principals map[string]*types.Principal
|
|
unknownEmails map[int]map[string]bool
|
|
labels map[string]int64 // map for labels {"label.key":label.id,}
|
|
labelValues map[int64]map[string]*int64 // map for label values {label.id:{"value-key":value-id,}}
|
|
migrator types.Principal
|
|
scope int64 // depth of space used for labels
|
|
}
|
|
|
|
// Import load provided pull requests in go-scm format and imports them.
|
|
//
|
|
//nolint:gocognit
|
|
func (migrate PullReq) Import(
|
|
ctx context.Context,
|
|
migrator types.Principal,
|
|
repo *types.Repository,
|
|
extPullReqs []*ExternalPullRequest,
|
|
) ([]*types.PullReq, error) {
|
|
readParams := git.ReadParams{RepoUID: repo.GitUID}
|
|
|
|
repoState := repoImportState{
|
|
git: migrate.git,
|
|
readParams: readParams,
|
|
principalStore: migrate.principalStore,
|
|
spaceStore: migrate.spaceStore,
|
|
pullReqActivityStore: migrate.pullReqActStore,
|
|
labelStore: migrate.labelStore,
|
|
labelValueStore: migrate.labelValueStore,
|
|
pullReqLabelAssignmentStore: migrate.pullReqLabelAssignmentStore,
|
|
branchCheck: map[string]*git.Branch{},
|
|
principals: map[string]*types.Principal{},
|
|
unknownEmails: map[int]map[string]bool{},
|
|
labels: map[string]int64{},
|
|
labelValues: map[int64]map[string]*int64{},
|
|
migrator: migrator,
|
|
scope: 0,
|
|
}
|
|
|
|
pullReqUnique := map[int]ExternalPullRequest{}
|
|
pullReqComments := map[*types.PullReq][]ExternalComment{}
|
|
|
|
pullReqs := make([]*types.PullReq, 0, len(extPullReqs))
|
|
// create the PR objects, one by one. Each pull request will mutate the repository object (to update the counters).
|
|
for _, extPullReqData := range extPullReqs {
|
|
extPullReq := &extPullReqData.PullRequest
|
|
|
|
if _, exists := pullReqUnique[extPullReq.Number]; exists {
|
|
return nil, errors.Conflict("duplicate pull request number %d", extPullReq.Number)
|
|
}
|
|
pullReqUnique[extPullReq.Number] = *extPullReqData
|
|
|
|
pr, err := repoState.convertPullReq(ctx, repo, extPullReqData)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to import pull request %d: %w", extPullReq.Number, err)
|
|
}
|
|
|
|
pullReqs = append(pullReqs, pr)
|
|
pullReqComments[pr] = extPullReqData.Comments
|
|
}
|
|
|
|
if len(pullReqs) == 0 { // nothing to do: exit early to avoid accessing the database
|
|
return nil, nil
|
|
}
|
|
|
|
err := migrate.tx.WithTx(ctx, func(ctx context.Context) error {
|
|
var deltaOpen, deltaClosed, deltaMerged int
|
|
var maxNumber int64
|
|
|
|
// Store the pull request objects and the comments.
|
|
for _, pullReq := range pullReqs {
|
|
if err := migrate.pullReqStore.Create(ctx, pullReq); err != nil {
|
|
return fmt.Errorf("failed to import the pull request %d: %w", pullReq.Number, err)
|
|
}
|
|
|
|
switch pullReq.State {
|
|
case enum.PullReqStateOpen:
|
|
deltaOpen++
|
|
case enum.PullReqStateClosed:
|
|
deltaClosed++
|
|
case enum.PullReqStateMerged:
|
|
deltaMerged++
|
|
}
|
|
|
|
if maxNumber < pullReq.Number {
|
|
maxNumber = pullReq.Number
|
|
}
|
|
|
|
comments, err := repoState.createComments(ctx, repo, pullReq, pullReqComments[pullReq])
|
|
if err != nil {
|
|
return fmt.Errorf("failed to import pull request comments: %w", err)
|
|
}
|
|
|
|
// Add a comment if any principal (PR author or commenter) were replaced by the fallback migrator principal
|
|
if prUnknownEmails, ok := repoState.unknownEmails[int(pullReq.Number)]; ok && len(prUnknownEmails) != 0 {
|
|
infoComment, err := repoState.createInfoComment(ctx, repo, pullReq)
|
|
if err != nil {
|
|
log.Ctx(ctx).Warn().Err(err).Msg("failed to add an informational comment for replacing non-existing users")
|
|
} else {
|
|
comments = append(comments, infoComment)
|
|
}
|
|
}
|
|
|
|
prLabels := pullReqUnique[int(pullReq.Number)].PullRequest.Labels
|
|
err = repoState.assignLabels(ctx, repo.ParentID, pullReq, prLabels)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to assign pull request %d labels: %w", pullReq.Number, err)
|
|
}
|
|
|
|
// no need to update the pull request object in the DB if there are no comments.
|
|
if len(comments) == 0 && len(prLabels) == 0 {
|
|
continue
|
|
}
|
|
|
|
if err := migrate.pullReqStore.Update(ctx, pullReq); err != nil {
|
|
return fmt.Errorf("failed to update pull request after importing of the comments: %w", err)
|
|
}
|
|
}
|
|
|
|
// Update the repository
|
|
|
|
repoUpdate, err := migrate.repoStore.Find(ctx, repo.ID)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to fetch repo in pull request import: %w", err)
|
|
}
|
|
|
|
if repoUpdate.PullReqSeq < maxNumber {
|
|
repoUpdate.PullReqSeq = maxNumber
|
|
}
|
|
repoUpdate.NumPulls += len(pullReqs)
|
|
repoUpdate.NumOpenPulls += deltaOpen
|
|
repoUpdate.NumClosedPulls += deltaClosed
|
|
repoUpdate.NumMergedPulls += deltaMerged
|
|
|
|
if err := migrate.repoStore.Update(ctx, repoUpdate); err != nil {
|
|
return fmt.Errorf("failed to update repo in pull request import: %w", err)
|
|
}
|
|
|
|
return nil
|
|
})
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return pullReqs, nil
|
|
}
|
|
|
|
// convertPullReq analyses external pull request object and creates types.PullReq object out of it.
|
|
func (r *repoImportState) convertPullReq(
|
|
ctx context.Context,
|
|
repo *types.Repository,
|
|
extPullReqData *ExternalPullRequest,
|
|
) (*types.PullReq, error) {
|
|
extPullReq := extPullReqData.PullRequest
|
|
|
|
log := log.Ctx(ctx).With().
|
|
Str("repo.identifier", repo.Identifier).
|
|
Int("pullreq.number", extPullReq.Number).
|
|
Logger()
|
|
|
|
author, err := r.getPrincipalByEmail(ctx, extPullReq.Author.Email, extPullReq.Number, false)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to get pull request author: %w", err)
|
|
}
|
|
|
|
now := time.Now().UnixMilli()
|
|
createdAt := timestampMillis(extPullReq.Created, now)
|
|
updatedAt := timestampMillis(extPullReq.Updated, now)
|
|
|
|
const maxTitleLen = 256
|
|
const maxDescriptionLen = 100000 // This limit is deliberately higher than the limit in our API.
|
|
if len(extPullReq.Title) > maxTitleLen {
|
|
extPullReq.Title = extPullReq.Title[:maxTitleLen]
|
|
}
|
|
if len(extPullReq.Body) > maxDescriptionLen {
|
|
extPullReq.Body = extPullReq.Body[:maxDescriptionLen]
|
|
}
|
|
|
|
pr := &types.PullReq{
|
|
ID: 0, // the ID will be populated in the data layer
|
|
Version: 0,
|
|
Number: int64(extPullReq.Number),
|
|
CreatedBy: author.ID,
|
|
Created: createdAt,
|
|
Updated: updatedAt,
|
|
Edited: updatedAt,
|
|
Closed: nil,
|
|
State: enum.PullReqStateOpen,
|
|
IsDraft: extPullReq.Draft,
|
|
CommentCount: 0,
|
|
UnresolvedCount: 0,
|
|
Title: extPullReq.Title,
|
|
Description: extPullReq.Body,
|
|
SourceRepoID: repo.ID,
|
|
SourceBranch: extPullReq.Head.Name,
|
|
SourceSHA: extPullReq.Head.SHA,
|
|
TargetRepoID: repo.ID,
|
|
TargetBranch: extPullReq.Base.Name,
|
|
ActivitySeq: 0,
|
|
// Merge related fields are all left unset and will be set depending on the PR state
|
|
}
|
|
|
|
params := git.ReadParams{RepoUID: repo.GitUID}
|
|
|
|
// Set the state of the PR
|
|
switch {
|
|
case extPullReq.Merged:
|
|
pr.State = enum.PullReqStateMerged
|
|
case extPullReq.Closed:
|
|
pr.State = enum.PullReqStateClosed
|
|
default:
|
|
pr.State = enum.PullReqStateOpen
|
|
}
|
|
|
|
// Update the PR depending on its state
|
|
switch pr.State {
|
|
case enum.PullReqStateMerged:
|
|
// For merged PR's assume the Head.Sha and Base.Sha point to commits at the time of merging.
|
|
|
|
pr.Merged = &pr.Updated
|
|
pr.MergedBy = &author.ID // Don't have real info for this - use the author.
|
|
mergeMethod := enum.MergeMethodMerge // Don't know
|
|
pr.MergeMethod = &mergeMethod
|
|
|
|
pr.SourceSHA = extPullReq.Head.SHA
|
|
pr.MergeTargetSHA = &extPullReq.Base.SHA
|
|
pr.MergeBaseSHA = extPullReq.Base.SHA
|
|
pr.MergeSHA = nil // Don't have this.
|
|
pr.MarkAsMerged()
|
|
|
|
case enum.PullReqStateClosed:
|
|
// For closed PR's it's not important to verify existence of branches and commits.
|
|
// If these don't exist the PR will be impossible to open.
|
|
pr.SourceSHA = extPullReq.Head.SHA
|
|
pr.MergeTargetSHA = &extPullReq.Base.SHA
|
|
pr.MergeBaseSHA = extPullReq.Base.SHA
|
|
pr.MergeSHA = nil
|
|
pr.MergeConflicts = nil
|
|
pr.MergeTargetSHA = nil
|
|
pr.MarkAsMergeUnchecked()
|
|
|
|
pr.Closed = &pr.Updated
|
|
|
|
case enum.PullReqStateOpen:
|
|
// For open PR we need to verify existence of branches and find to merge base.
|
|
|
|
sourceBranch, err := r.git.GetBranch(ctx, &git.GetBranchParams{
|
|
ReadParams: params,
|
|
BranchName: extPullReq.Head.Name,
|
|
})
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to fetch source branch of an open pull request: %w", err)
|
|
}
|
|
|
|
// TODO: Cache this in the repoImportState - it's very likely that it will be the same for other PRs
|
|
targetBranch, err := r.git.GetBranch(ctx, &git.GetBranchParams{
|
|
ReadParams: params,
|
|
BranchName: extPullReq.Base.Name,
|
|
})
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to fetch target branch of an open pull request: %w", err)
|
|
}
|
|
|
|
mergeBase, err := r.git.MergeBase(ctx, git.MergeBaseParams{
|
|
ReadParams: params,
|
|
Ref1: sourceBranch.Branch.SHA.String(),
|
|
Ref2: targetBranch.Branch.SHA.String(),
|
|
})
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to find merge base an open pull request: %w", err)
|
|
}
|
|
|
|
sourceSHA := sourceBranch.Branch.SHA.String()
|
|
targetSHA := targetBranch.Branch.SHA.String()
|
|
pr.SourceSHA = sourceSHA
|
|
pr.MergeTargetSHA = &targetSHA
|
|
pr.MergeBaseSHA = mergeBase.MergeBaseSHA.String()
|
|
pr.MarkAsMergeUnchecked()
|
|
}
|
|
|
|
log.Debug().Str("pullreq.state", string(pr.State)).Msg("importing pull request")
|
|
|
|
return pr, nil
|
|
}
|
|
|
|
// createComments analyses external pull request comment objects and stores types.PullReqActivity object to the DB.
|
|
// It will mutate the pull request object to update counter fields.
|
|
func (r *repoImportState) createComments(
|
|
ctx context.Context,
|
|
repo *types.Repository,
|
|
pullReq *types.PullReq,
|
|
extComments []ExternalComment,
|
|
) ([]*types.PullReqActivity, error) {
|
|
log := log.Ctx(ctx).With().
|
|
Str("repo.id", repo.Identifier).
|
|
Int("pullreq.number", int(pullReq.Number)).
|
|
Logger()
|
|
|
|
extThreads := generateThreads(extComments)
|
|
|
|
comments := make([]*types.PullReqActivity, 0, len(extComments))
|
|
for idxTopLevel, extThread := range extThreads {
|
|
order := idxTopLevel + 1
|
|
|
|
// Create the top level comment with the correct value of Order, SubOrder and ReplySeq.
|
|
commentTopLevel, err := r.createComment(ctx, repo, pullReq, nil,
|
|
order, 0, len(extThread.Replies), &extThread.TopLevel)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to create top level comment: %w", err)
|
|
}
|
|
|
|
comments = append(comments, commentTopLevel)
|
|
|
|
for idxReply, extReply := range extThread.Replies {
|
|
subOrder := idxReply + 1
|
|
|
|
// Create the reply comment with the correct value of Order, SubOrder and ReplySeq.
|
|
//nolint:gosec
|
|
commentReply, err := r.createComment(ctx, repo, pullReq, &commentTopLevel.ID,
|
|
order, subOrder, 0, &extReply)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to create reply comment: %w", err)
|
|
}
|
|
|
|
comments = append(comments, commentReply)
|
|
}
|
|
}
|
|
|
|
log.Debug().Int("count", len(comments)).Msg("imported pull request comments")
|
|
|
|
return comments, nil
|
|
}
|
|
|
|
// createComment analyses an external pull request comment object and creates types.PullReqActivity object out of it.
|
|
// It will mutate the pull request object to update counter fields.
|
|
func (r *repoImportState) createComment(
|
|
ctx context.Context,
|
|
repo *types.Repository,
|
|
pullReq *types.PullReq,
|
|
parentID *int64,
|
|
order, subOrder, replySeq int,
|
|
extComment *ExternalComment,
|
|
) (*types.PullReqActivity, error) {
|
|
commenter, err := r.getPrincipalByEmail(ctx, extComment.Author.Email, int(pullReq.Number), false)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to get comment ID=%d author: %w", extComment.ID, err)
|
|
}
|
|
commentedAt := extComment.Created.UnixMilli()
|
|
|
|
// Mark comments as resolved if the PR is merged, otherwise they are unresolved.
|
|
var resolved, resolvedBy *int64
|
|
if pullReq.State == enum.PullReqStateMerged {
|
|
resolved = &commentedAt
|
|
resolvedBy = &commenter.ID
|
|
}
|
|
|
|
const maxLenText = 64 << 10 // This limit is deliberately larger than the limit in our API.
|
|
if len(extComment.Body) > maxLenText {
|
|
extComment.Body = extComment.Body[:maxLenText]
|
|
}
|
|
|
|
comment := &types.PullReqActivity{
|
|
CreatedBy: commenter.ID,
|
|
Created: commentedAt,
|
|
Updated: commentedAt,
|
|
Edited: commentedAt,
|
|
Deleted: nil,
|
|
ParentID: parentID,
|
|
RepoID: repo.ID,
|
|
PullReqID: pullReq.ID,
|
|
Order: int64(order),
|
|
SubOrder: int64(subOrder),
|
|
ReplySeq: int64(replySeq),
|
|
Type: enum.PullReqActivityTypeComment,
|
|
Kind: enum.PullReqActivityKindComment,
|
|
Text: extComment.Body,
|
|
PayloadRaw: json.RawMessage("{}"),
|
|
Metadata: nil,
|
|
ResolvedBy: resolvedBy,
|
|
Resolved: resolved,
|
|
CodeComment: nil,
|
|
Mentions: nil,
|
|
}
|
|
|
|
if cc := extComment.CodeComment; cc != nil && cc.HunkHeader != "" && extComment.ParentID == 0 {
|
|
// a code comment must have a valid HunkHeader and must not be a reply
|
|
hunkHeader, ok := parser.ParseDiffHunkHeader(cc.HunkHeader)
|
|
if !ok {
|
|
return nil, errors.InvalidArgument("Invalid hunk header for code comment: %s", cc.HunkHeader)
|
|
}
|
|
|
|
comment.Kind = enum.PullReqActivityKindChangeComment
|
|
comment.Type = enum.PullReqActivityTypeCodeComment
|
|
|
|
comment.CodeComment = &types.CodeCommentFields{
|
|
Outdated: cc.SourceSHA != pullReq.SourceSHA,
|
|
MergeBaseSHA: cc.MergeBaseSHA,
|
|
SourceSHA: cc.SourceSHA,
|
|
Path: cc.Path,
|
|
LineNew: hunkHeader.NewLine,
|
|
SpanNew: hunkHeader.NewSpan,
|
|
LineOld: hunkHeader.OldLine,
|
|
SpanOld: hunkHeader.OldSpan,
|
|
}
|
|
|
|
sideNew := !strings.EqualFold(cc.Side, "OLD") // cc.Side can be either OLD or NEW
|
|
_ = comment.SetPayload(&types.PullRequestActivityPayloadCodeComment{
|
|
Title: cc.CodeSnippet.Header,
|
|
Lines: cc.CodeSnippet.Lines,
|
|
LineStartNew: sideNew,
|
|
LineEndNew: sideNew,
|
|
})
|
|
}
|
|
|
|
// store the comment
|
|
|
|
if err := r.pullReqActivityStore.Create(ctx, comment); err != nil {
|
|
return nil, fmt.Errorf("failed to store the external comment ID=%d author: %w", extComment.ID, err)
|
|
}
|
|
|
|
// update the pull request's counter fields
|
|
|
|
pullReq.CommentCount++
|
|
if comment.IsBlocking() {
|
|
pullReq.UnresolvedCount++
|
|
}
|
|
|
|
if pullReq.ActivitySeq < comment.Order {
|
|
pullReq.ActivitySeq = comment.Order
|
|
}
|
|
|
|
return comment, nil
|
|
}
|
|
|
|
// createInfoComment creates an informational comment on the PR
|
|
// if any of the principals were replaced with the migrator.
|
|
func (r *repoImportState) createInfoComment(
|
|
ctx context.Context,
|
|
repo *types.Repository,
|
|
pullReq *types.PullReq,
|
|
) (*types.PullReqActivity, error) {
|
|
var unknownEmails []string
|
|
for email := range r.unknownEmails[int(pullReq.Number)] {
|
|
unknownEmails = append(unknownEmails, email)
|
|
}
|
|
now := time.Now().UnixMilli()
|
|
text := fmt.Sprintf(InfoCommentMessage, r.migrator.UID, strings.Join(unknownEmails, ", "))
|
|
comment := &types.PullReqActivity{
|
|
CreatedBy: r.migrator.ID,
|
|
Created: now,
|
|
Updated: now,
|
|
Deleted: nil,
|
|
ParentID: nil,
|
|
RepoID: repo.ID,
|
|
PullReqID: pullReq.ID,
|
|
Order: pullReq.ActivitySeq + 1,
|
|
SubOrder: 0,
|
|
ReplySeq: 0,
|
|
Type: enum.PullReqActivityTypeComment,
|
|
Kind: enum.PullReqActivityKindComment,
|
|
Text: text,
|
|
PayloadRaw: json.RawMessage("{}"),
|
|
Metadata: nil,
|
|
ResolvedBy: &r.migrator.ID,
|
|
Resolved: &now,
|
|
CodeComment: nil,
|
|
Mentions: nil,
|
|
}
|
|
|
|
if err := r.pullReqActivityStore.Create(ctx, comment); err != nil {
|
|
return nil, fmt.Errorf("failed to store the info comment author: %w", err)
|
|
}
|
|
|
|
pullReq.ActivitySeq++
|
|
pullReq.CommentCount++
|
|
|
|
return comment, nil
|
|
}
|
|
|
|
func (r *repoImportState) getPrincipalByEmail(
|
|
ctx context.Context,
|
|
emailAddress string,
|
|
prNumber int,
|
|
strict bool,
|
|
) (*types.Principal, error) {
|
|
if principal, exists := r.principals[emailAddress]; exists {
|
|
return principal, nil
|
|
}
|
|
|
|
principal, err := r.principalStore.FindByEmail(ctx, emailAddress)
|
|
if err != nil && !errors.Is(err, gitness_store.ErrResourceNotFound) {
|
|
return nil, fmt.Errorf("failed to load principal by email: %w", err)
|
|
}
|
|
|
|
if err == nil {
|
|
r.principals[emailAddress] = principal
|
|
return principal, nil
|
|
}
|
|
|
|
if strict {
|
|
return nil, fmt.Errorf(
|
|
"could not find principal by email %s and automatic replacing unknown prinicapls is disabled: %w",
|
|
emailAddress, err)
|
|
}
|
|
|
|
// ignore not found emails if is not strict
|
|
if _, exists := r.unknownEmails[prNumber]; !exists {
|
|
r.unknownEmails[prNumber] = make(map[string]bool, 0)
|
|
}
|
|
|
|
if _, ok := r.unknownEmails[prNumber][emailAddress]; !ok && len(r.unknownEmails[prNumber]) < MaxNumberOfUnknownEmails {
|
|
r.unknownEmails[prNumber][emailAddress] = true
|
|
}
|
|
|
|
return &r.migrator, nil
|
|
}
|
|
|
|
func (r *repoImportState) assignLabels(
|
|
ctx context.Context,
|
|
spaceID int64,
|
|
pullreq *types.PullReq,
|
|
labels []ExternalLabel,
|
|
) error {
|
|
if len(labels) == 0 {
|
|
return nil
|
|
}
|
|
now := time.Now().UnixMilli()
|
|
for _, l := range labels {
|
|
var label *types.Label
|
|
var err error
|
|
labelID, found := r.labels[l.Name]
|
|
if !found {
|
|
label, err = r.labelStore.Find(ctx, &spaceID, nil, l.Name)
|
|
if errors.Is(err, gitness_store.ErrResourceNotFound) {
|
|
label, err = r.defineLabel(ctx, spaceID, l)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to define label: %w", err)
|
|
}
|
|
} else if err != nil {
|
|
return fmt.Errorf("failed to find the label with key %s in space %d: %w", l.Name, spaceID, err)
|
|
}
|
|
|
|
r.labels[l.Name], labelID = label.ID, label.ID
|
|
}
|
|
|
|
var valueID *int64
|
|
valueID, found = r.labelValues[labelID][l.Value]
|
|
if !found && l.Value != "" {
|
|
var labelValue *types.LabelValue
|
|
labelValue, err = r.labelValueStore.FindByLabelID(ctx, labelID, l.Value)
|
|
if errors.Is(err, gitness_store.ErrResourceNotFound) {
|
|
labelValue, err = r.defineLabelValue(ctx, labelID, l.Value)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to define label values: %w", err)
|
|
}
|
|
} else if err != nil {
|
|
return fmt.Errorf("failed to find the label with value %s and key %s in space %d: %w",
|
|
l.Value, l.Name, spaceID, err)
|
|
}
|
|
|
|
valueID = &labelValue.ID
|
|
}
|
|
|
|
pullReqLabel := &types.PullReqLabel{
|
|
PullReqID: pullreq.ID,
|
|
LabelID: labelID,
|
|
ValueID: valueID,
|
|
Created: now,
|
|
Updated: now,
|
|
CreatedBy: r.migrator.ID,
|
|
UpdatedBy: r.migrator.ID,
|
|
}
|
|
|
|
err = r.pullReqLabelAssignmentStore.Assign(ctx, pullReqLabel)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to assign label %s to pull request: %w", l.Name, err)
|
|
}
|
|
pullreq.ActivitySeq++
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func (r *repoImportState) defineLabel(
|
|
ctx context.Context,
|
|
spaceID int64,
|
|
extLabel ExternalLabel,
|
|
) (*types.Label, error) {
|
|
if r.scope == 0 {
|
|
spaceIDs, err := r.spaceStore.GetAncestorIDs(ctx, spaceID)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to get space ids hierarchy: %w", err)
|
|
}
|
|
|
|
r.scope = int64(len(spaceIDs))
|
|
}
|
|
|
|
label, err := convertLabelWithSanitization(ctx, r.migrator, spaceID, r.scope, extLabel)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to sanitize and convert external label input: %w", err)
|
|
}
|
|
|
|
label, err = r.labelStore.Find(ctx, &spaceID, nil, label.Key)
|
|
if errors.Is(err, gitness_store.ErrResourceNotFound) {
|
|
err = r.labelStore.Define(ctx, label)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to define and find the label: %w", err)
|
|
}
|
|
}
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to define and find the label: %w", err)
|
|
}
|
|
|
|
return label, nil
|
|
}
|
|
|
|
func (r *repoImportState) defineLabelValue(
|
|
ctx context.Context,
|
|
labelID int64,
|
|
value string,
|
|
) (*types.LabelValue, error) {
|
|
valueIn := &types.DefineValueInput{
|
|
Value: value,
|
|
Color: defaultLabelValueColor,
|
|
}
|
|
if err := valueIn.Sanitize(); err != nil {
|
|
return nil, fmt.Errorf("failed to sanitize external label value input: %w", err)
|
|
}
|
|
|
|
if _, exists := r.labelValues[labelID]; !exists {
|
|
r.labelValues[labelID] = make(map[string]*int64)
|
|
}
|
|
|
|
labelValue, err := r.labelValueStore.FindByLabelID(ctx, labelID, valueIn.Value)
|
|
if err == nil {
|
|
r.labelValues[labelID][labelValue.Value] = &labelValue.ID
|
|
return labelValue, nil
|
|
}
|
|
|
|
if !errors.Is(err, gitness_store.ErrResourceNotFound) {
|
|
return nil, fmt.Errorf("failed to fine label value: %w", err)
|
|
}
|
|
|
|
// define the label value if not exists
|
|
now := time.Now().UnixMilli()
|
|
labelValue = &types.LabelValue{
|
|
LabelID: labelID,
|
|
Value: valueIn.Value,
|
|
Color: defaultLabelValueColor,
|
|
Created: now,
|
|
Updated: now,
|
|
CreatedBy: r.migrator.ID,
|
|
UpdatedBy: r.migrator.ID,
|
|
}
|
|
err = r.labelValueStore.Define(ctx, labelValue)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to define label value: %w", err)
|
|
}
|
|
_, err = r.labelStore.IncrementValueCount(ctx, labelID, 1)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to update label value count: %w", err)
|
|
}
|
|
|
|
r.labelValues[labelID][labelValue.Value] = &labelValue.ID
|
|
return labelValue, nil
|
|
}
|
|
|
|
func timestampMillis(t time.Time, def int64) int64 {
|
|
if t.IsZero() {
|
|
return def
|
|
}
|
|
|
|
return t.UnixMilli()
|
|
}
|
|
|
|
func generateThreads(extComments []ExternalComment) []*externalCommentThread {
|
|
extCommentParents := make(map[int]int, len(extComments))
|
|
extCommentMap := make(map[int]ExternalComment, len(extComments))
|
|
for _, extComment := range extComments {
|
|
extCommentParents[extComment.ID] = extComment.ParentID
|
|
extCommentMap[extComment.ID] = extComment
|
|
}
|
|
|
|
// Make flat list of reply comment IDs: create map[topLevelCommentID]->[]commentID
|
|
extCommentIDReplyMap := make(map[int][]int)
|
|
for _, extComment := range extComments {
|
|
topLevelParentID := getTopLevelParentID(extComment.ID, extCommentParents)
|
|
if topLevelParentID < 0 {
|
|
continue
|
|
}
|
|
if topLevelParentID == extComment.ID {
|
|
// Make sure the item with topLevelParentID exist in the map, at least as a nil entry.
|
|
extCommentIDReplyMap[topLevelParentID] = extCommentIDReplyMap[topLevelParentID] //nolint:staticcheck
|
|
continue
|
|
}
|
|
extCommentIDReplyMap[topLevelParentID] = append(extCommentIDReplyMap[topLevelParentID], extComment.ID)
|
|
}
|
|
|
|
countTopLevel := len(extCommentIDReplyMap)
|
|
if countTopLevel == 0 {
|
|
return nil
|
|
}
|
|
|
|
extCommentThreads := make([]*externalCommentThread, 0, countTopLevel)
|
|
for topLevelID, replyIDs := range extCommentIDReplyMap {
|
|
expReplyComments := make([]ExternalComment, len(replyIDs))
|
|
for i, replyID := range replyIDs {
|
|
expReplyComments[i] = extCommentMap[replyID]
|
|
}
|
|
thread := &externalCommentThread{
|
|
TopLevel: extCommentMap[topLevelID],
|
|
Replies: expReplyComments,
|
|
}
|
|
extCommentThreads = append(extCommentThreads, thread)
|
|
}
|
|
|
|
// order top level comments
|
|
|
|
sort.Slice(extCommentThreads, func(i, j int) bool {
|
|
created1 := extCommentThreads[i].TopLevel.Created
|
|
created2 := extCommentThreads[j].TopLevel.Created
|
|
return created1.Before(created2)
|
|
})
|
|
|
|
// order reply comments
|
|
|
|
for _, thread := range extCommentThreads {
|
|
sort.Slice(thread.Replies, func(i, j int) bool {
|
|
created1 := thread.Replies[i].Created
|
|
created2 := thread.Replies[j].Created
|
|
return created1.Before(created2)
|
|
})
|
|
}
|
|
|
|
return extCommentThreads
|
|
}
|
|
|
|
func getTopLevelParentID(id int, tree map[int]int) int {
|
|
const maxDepth = 20
|
|
for currID, depth := id, 0; depth < maxDepth; depth++ {
|
|
parentID := tree[currID]
|
|
if parentID == 0 {
|
|
return currID
|
|
}
|
|
|
|
currID = parentID
|
|
}
|
|
|
|
return -1
|
|
}
|