2023-11-15 10:15:32 +00:00

253 lines
7.6 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 codecomments
import (
"context"
"github.com/harness/gitness/errors"
"github.com/harness/gitness/git"
gitenum "github.com/harness/gitness/git/enum"
"github.com/harness/gitness/types"
"github.com/rs/zerolog/log"
)
// Migrator is a utility used to migrate code comments after update of the pull request's source branch.
type Migrator struct {
hunkHeaderFetcher hunkHeaderFetcher
}
type hunkHeaderFetcher interface {
GetDiffHunkHeaders(context.Context, git.GetDiffHunkHeadersParams) (git.GetDiffHunkHeadersOutput, error)
}
// MigrateNew updates the "+" (the added lines) part of code comments
// after a new commit on the pull request's source branch.
// The parameter newSHA should contain the latest commit SHA of the pull request's source branch.
func (migrator *Migrator) MigrateNew(
ctx context.Context,
repoGitUID string,
newSHA string,
comments []*types.CodeComment,
) {
migrator.migrate(
ctx,
repoGitUID,
newSHA,
comments,
func(codeComment *types.CodeComment) string {
return codeComment.SourceSHA
},
func(codeComment *types.CodeComment, sha string) {
codeComment.SourceSHA = sha
},
func(codeComment *types.CodeComment) (int, int) {
return codeComment.LineNew, codeComment.LineNew + codeComment.SpanNew - 1
},
func(codeComment *types.CodeComment, line int) {
codeComment.LineNew += line
},
)
}
// MigrateOld updates the "-" (the removes lines) part of code comments
// after the pull request's change of the merge base commit.
func (migrator *Migrator) MigrateOld(
ctx context.Context,
repoGitUID string,
newSHA string,
comments []*types.CodeComment,
) {
migrator.migrate(
ctx,
repoGitUID,
newSHA,
comments,
func(codeComment *types.CodeComment) string {
return codeComment.MergeBaseSHA
},
func(codeComment *types.CodeComment, sha string) {
codeComment.MergeBaseSHA = sha
},
func(codeComment *types.CodeComment) (int, int) {
return codeComment.LineOld, codeComment.LineOld + codeComment.SpanOld - 1
},
func(codeComment *types.CodeComment, line int) {
codeComment.LineOld += line
},
)
}
//nolint:gocognit,funlen // refactor if needed
func (migrator *Migrator) migrate(
ctx context.Context,
repoGitUID string,
newSHA string,
comments []*types.CodeComment,
getSHA func(codeComment *types.CodeComment) string,
setSHA func(codeComment *types.CodeComment, sha string),
getCommentStartEnd func(codeComment *types.CodeComment) (int, int),
updateCommentLine func(codeComment *types.CodeComment, line int),
) {
if len(comments) == 0 {
return
}
commitMap, initialValuesMap := mapCodeComments(comments, getSHA)
for commentSHA, fileMap := range commitMap {
// get all hunk headers for the diff between the SHA that's stored in the comment and the new SHA.
diffSummary, errDiff := migrator.hunkHeaderFetcher.GetDiffHunkHeaders(ctx, git.GetDiffHunkHeadersParams{
ReadParams: git.ReadParams{
RepoUID: repoGitUID,
},
SourceCommitSHA: commentSHA,
TargetCommitSHA: newSHA,
})
if errors.AsStatus(errDiff) == errors.StatusNotFound {
// Handle the commit SHA not found error and mark all code comments as outdated.
for _, codeComments := range fileMap {
for _, codeComment := range codeComments {
codeComment.Outdated = true
}
}
continue
}
if errDiff != nil {
log.Ctx(ctx).Err(errDiff).
Msgf("failed to get git diff between comment's sha %s and the latest %s", commentSHA, newSHA)
continue
}
// Traverse all the changed files
for _, file := range diffSummary.Files {
var codeComments []*types.CodeComment
codeComments = fileMap[file.FileHeader.OldName]
// Handle file renames
if file.FileHeader.OldName != file.FileHeader.NewName {
if len(codeComments) == 0 {
// If the code comments are not found using the old name of the file, try with the new name.
codeComments = fileMap[file.FileHeader.NewName]
} else {
// Update the code comment's path to the new file name
for _, cc := range codeComments {
cc.Path = file.FileHeader.NewName
}
}
}
// Handle file delete
if _, isDeleted := file.FileHeader.Extensions[gitenum.DiffExtHeaderDeletedFileMode]; isDeleted {
for _, codeComment := range codeComments {
codeComment.Outdated = true
}
continue
}
// Handle new files - shouldn't happen because no code comments should exist for a non-existing file.
if _, isAdded := file.FileHeader.Extensions[gitenum.DiffExtHeaderNewFileMode]; isAdded {
for _, codeComment := range codeComments {
codeComment.Outdated = true
}
continue
}
for hunkIdx := len(file.HunkHeaders) - 1; hunkIdx >= 0; hunkIdx-- {
hunk := file.HunkHeaders[hunkIdx]
for _, cc := range codeComments {
if cc.Outdated {
continue
}
ccStart, ccEnd := getCommentStartEnd(cc)
outdated, moveDelta := processCodeComment(ccStart, ccEnd, hunk)
if outdated {
cc.CodeCommentFields = initialValuesMap[cc.ID] // revert the CC to the original values
cc.Outdated = true
continue
}
updateCommentLine(cc, moveDelta)
}
}
}
for _, codeComments := range fileMap {
for _, codeComment := range codeComments {
if codeComment.Outdated {
continue
}
setSHA(codeComment, newSHA)
}
}
}
}
// mapCodeComments groups code comments to maps, first by commit SHA and then by file name.
// It assumes the incoming list is already sorted.
func mapCodeComments(
comments []*types.CodeComment,
extractSHA func(*types.CodeComment) string,
) (map[string]map[string][]*types.CodeComment, map[int64]types.CodeCommentFields) {
commitMap := map[string]map[string][]*types.CodeComment{}
originalComments := make(map[int64]types.CodeCommentFields, len(comments))
for _, comment := range comments {
commitSHA := extractSHA(comment)
fileMap := commitMap[commitSHA]
if fileMap == nil {
fileMap = map[string][]*types.CodeComment{}
}
fileComments := fileMap[comment.Path]
fileComments = append(fileComments, comment)
fileMap[comment.Path] = fileComments
commitMap[commitSHA] = fileMap
originalComments[comment.ID] = comment.CodeCommentFields
}
return commitMap, originalComments
}
func processCodeComment(ccStart, ccEnd int, h git.HunkHeader) (outdated bool, moveDelta int) {
// A code comment is marked as outdated if:
// * The code lines covered by the code comment are changed
// (the range given by the OldLine/OldSpan overlaps the code comment's code range)
// * There are new lines inside the line range covered by the code comment, don't care about how many
// (the NewLine is between the CC start and CC end; the value of the NewSpan is unimportant).
outdated =
(h.OldSpan > 0 && ccEnd >= h.OldLine && ccStart <= h.OldLine+h.OldSpan-1) || // code comment's code is changed
(h.NewSpan > 0 && h.NewLine > ccStart && h.NewLine <= ccEnd) // lines are added inside the code comment
if outdated {
return // outdated comments aren't moved
}
if ccEnd <= h.OldLine {
return // the change described by the hunk header is below the code comment, so it doesn't affect it
}
moveDelta = h.NewSpan - h.OldSpan
return
}