private async Task<LinkedFileMergeResult> MergeLinkedDocumentGroupAsync(
            IEnumerable<DocumentId> allLinkedDocuments,
            IEnumerable<DocumentId> linkedDocumentGroup,
            LinkedFileDiffMergingSessionInfo sessionInfo,
            IMergeConflictHandler mergeConflictHandler,
            CancellationToken cancellationToken)
        {
            var groupSessionInfo = new LinkedFileGroupSessionInfo();

            // Automatically merge non-conflicting diffs while collecting the conflicting diffs

            var textDifferencingService = _oldSolution.Workspace.Services.GetService<IDocumentTextDifferencingService>() ?? new DefaultDocumentTextDifferencingService();
            var appliedChanges = await textDifferencingService.GetTextChangesAsync(_oldSolution.GetDocument(linkedDocumentGroup.First()), _newSolution.GetDocument(linkedDocumentGroup.First()), cancellationToken).ConfigureAwait(false);
            var unmergedChanges = new List<UnmergedDocumentChanges>();

            foreach (var documentId in linkedDocumentGroup.Skip(1))
            {
                appliedChanges = await AddDocumentMergeChangesAsync(
                    _oldSolution.GetDocument(documentId),
                    _newSolution.GetDocument(documentId),
                    appliedChanges.ToList(),
                    unmergedChanges,
                    groupSessionInfo,
                    textDifferencingService,
                    cancellationToken).ConfigureAwait(false);
            }

            var originalDocument = _oldSolution.GetDocument(linkedDocumentGroup.First());
            var originalSourceText = await originalDocument.GetTextAsync(cancellationToken).ConfigureAwait(false);

            // Add comments in source explaining diffs that could not be merged

            IEnumerable<TextChange> allChanges;
            IList<TextSpan> mergeConflictResolutionSpan = new List<TextSpan>();

            if (unmergedChanges.Any())
            {
                mergeConflictHandler = mergeConflictHandler ?? _oldSolution.GetDocument(linkedDocumentGroup.First()).GetLanguageService<ILinkedFileMergeConflictCommentAdditionService>();
                var mergeConflictTextEdits = mergeConflictHandler.CreateEdits(originalSourceText, unmergedChanges);

                allChanges = MergeChangesWithMergeFailComments(appliedChanges, mergeConflictTextEdits, mergeConflictResolutionSpan, groupSessionInfo);
            }
            else
            {
                allChanges = appliedChanges;
            }

            groupSessionInfo.LinkedDocuments = _newSolution.GetDocumentIdsWithFilePath(originalDocument.FilePath).Length;
            groupSessionInfo.DocumentsWithChanges = linkedDocumentGroup.Count();
            sessionInfo.LogLinkedFileResult(groupSessionInfo);

            return new LinkedFileMergeResult(allLinkedDocuments, originalSourceText.WithChanges(allChanges), mergeConflictResolutionSpan);
        }
            private async Task<LinkedFileMergeResult> MergeLinkedDocumentGroupAsync(
                IEnumerable<DocumentId> linkedDocumentGroup,
                LinkedFileDiffMergingSessionInfo sessionInfo,
                CancellationToken cancellationToken)
            {
                var groupSessionInfo = new LinkedFileGroupSessionInfo();

                // Automatically merge non-conflicting diffs while collecting the conflicting diffs

                var appliedChanges = await newSolution.GetDocument(linkedDocumentGroup.First()).GetTextChangesAsync(oldSolution.GetDocument(linkedDocumentGroup.First())).ConfigureAwait(false);
                var unmergedChanges = new List<UnmergedDocumentChanges>();

                foreach (var documentId in linkedDocumentGroup.Skip(1))
                {
                    appliedChanges = await AddDocumentMergeChangesAsync(
                        oldSolution.GetDocument(documentId),
                        newSolution.GetDocument(documentId),
                        appliedChanges.ToList(),
                        unmergedChanges,
                        groupSessionInfo,
                        cancellationToken).ConfigureAwait(false);
                }

                var originalDocument = oldSolution.GetDocument(linkedDocumentGroup.First());
                var originalSourceText = await originalDocument.GetTextAsync().ConfigureAwait(false);

                // Add comments in source explaining diffs that could not be merged

                IEnumerable<TextChange> allChanges;
                if (unmergedChanges.Any())
                {
                    var mergeConflictCommentAdder = originalDocument.GetLanguageService<ILinkedFileMergeConflictCommentAdditionService>();
                    var commentChanges = mergeConflictCommentAdder.CreateCommentsForUnmergedChanges(originalSourceText, unmergedChanges);

                    allChanges = MergeChangesWithMergeFailComments(appliedChanges, commentChanges, groupSessionInfo);
                }
                else
                {
                    allChanges = appliedChanges;
                }

                groupSessionInfo.LinkedDocuments = newSolution.GetDocumentIdsWithFilePath(originalDocument.FilePath).Length;
                groupSessionInfo.DocumentsWithChanges = linkedDocumentGroup.Count();
                sessionInfo.LogLinkedFileResult(groupSessionInfo);

                return new LinkedFileMergeResult(originalSourceText.WithChanges(allChanges), hasMergeConflicts: unmergedChanges.Any());
            }
 public void LogLinkedFileResult(LinkedFileGroupSessionInfo info)
 {
     LinkedFileGroups.Add(info);
 }
                public static KeyValueLogMessage Create(int sessionId, LinkedFileGroupSessionInfo groupInfo)
                {
                    return KeyValueLogMessage.Create(m =>
                    {
                        m[SessionId] = sessionId.ToString();

                        m[LinkedDocuments] = groupInfo.LinkedDocuments.ToString();
                        m[DocumentsWithChanges] = groupInfo.DocumentsWithChanges.ToString();
                        m[IdenticalDiffs] = groupInfo.IdenticalDiffs.ToString();
                        m[IsolatedDiffs] = groupInfo.IsolatedDiffs.ToString();
                        m[OverlappingDistinctDiffs] = groupInfo.OverlappingDistinctDiffs.ToString();
                        m[OverlappingDistinctDiffsWithSameSpan] = groupInfo.OverlappingDistinctDiffsWithSameSpan.ToString();
                        m[OverlappingDistinctDiffsWithSameSpanAndSubstringRelation] = groupInfo.OverlappingDistinctDiffsWithSameSpanAndSubstringRelation.ToString();
                        m[InsertedMergeConflictComments] = groupInfo.InsertedMergeConflictComments.ToString();
                        m[InsertedMergeConflictCommentsAtAdjustedLocation] = groupInfo.InsertedMergeConflictCommentsAtAdjustedLocation.ToString();
                    });
                }
            private IEnumerable<TextChange> MergeChangesWithMergeFailComments(IEnumerable<TextChange> mergedChanges, IEnumerable<TextChange> commentChanges, LinkedFileGroupSessionInfo groupSessionInfo)
            {
                var mergedChangesList = NormalizeChanges(mergedChanges).ToList();
                var commentChangesList = NormalizeChanges(commentChanges).ToList();

                var combinedChanges = new List<TextChange>();
                var insertedMergeConflictCommentsAtAdjustedLocation = 0;

                var commentChangeIndex = 0;
                foreach (var mergedChange in mergedChangesList)
                {
                    while (commentChangeIndex < commentChangesList.Count && commentChangesList[commentChangeIndex].Span.End <= mergedChange.Span.Start)
                    {
                        // Add a comment change that does not conflict with any merge change
                        combinedChanges.Add(commentChangesList[commentChangeIndex]);
                        commentChangeIndex++;
                    }

                    if (commentChangeIndex >= commentChangesList.Count || mergedChange.Span.End <= commentChangesList[commentChangeIndex].Span.Start)
                    {
                        // Add a merge change that does not conflict with any comment change
                        combinedChanges.Add(mergedChange);
                        continue;
                    }

                    // The current comment insertion location conflicts with a merge diff location. Add the comment before the diff.
                    var conflictingCommentInsertionLocation = new TextSpan(mergedChange.Span.Start, 0);
                    while (commentChangeIndex < commentChangesList.Count && commentChangesList[commentChangeIndex].Span.Start < mergedChange.Span.End)
                    {
                        combinedChanges.Add(new TextChange(conflictingCommentInsertionLocation, commentChangesList[commentChangeIndex].NewText));
                        commentChangeIndex++;

                        insertedMergeConflictCommentsAtAdjustedLocation++;
                    }

                    combinedChanges.Add(mergedChange);
                }

                while (commentChangeIndex < commentChangesList.Count)
                {
                    // Add a comment change that does not conflict with any merge change
                    combinedChanges.Add(commentChangesList[commentChangeIndex]);
                    commentChangeIndex++;
                }

                groupSessionInfo.InsertedMergeConflictComments = commentChanges.Count();
                groupSessionInfo.InsertedMergeConflictCommentsAtAdjustedLocation = insertedMergeConflictCommentsAtAdjustedLocation;

                return NormalizeChanges(combinedChanges);
            }
            private static async Task<List<TextChange>> AddDocumentMergeChangesAsync(
                Document oldDocument,
                Document newDocument,
                List<TextChange> cumulativeChanges,
                List<UnmergedDocumentChanges> unmergedChanges,
                LinkedFileGroupSessionInfo groupSessionInfo,
                CancellationToken cancellationToken)
            {
                var unmergedDocumentChanges = new List<TextChange>();
                var successfullyMergedChanges = new List<TextChange>();

                int cumulativeChangeIndex = 0;
                foreach (var change in await newDocument.GetTextChangesAsync(oldDocument).ConfigureAwait(false))
                {
                    while (cumulativeChangeIndex < cumulativeChanges.Count && cumulativeChanges[cumulativeChangeIndex].Span.End < change.Span.Start)
                    {
                        // Existing change that does not overlap with the current change in consideration
                        successfullyMergedChanges.Add(cumulativeChanges[cumulativeChangeIndex]);
                        cumulativeChangeIndex++;

                        groupSessionInfo.IsolatedDiffs++;
                    }

                    if (cumulativeChangeIndex < cumulativeChanges.Count)
                    {
                        var cumulativeChange = cumulativeChanges[cumulativeChangeIndex];
                        if (!cumulativeChange.Span.IntersectsWith(change.Span))
                        {
                            // The current change in consideration does not intersect with any existing change
                            successfullyMergedChanges.Add(change);

                            groupSessionInfo.IsolatedDiffs++;
                        }
                        else
                        {
                            if (change.Span != cumulativeChange.Span || change.NewText != cumulativeChange.NewText)
                            {
                                // The current change in consideration overlaps an existing change but
                                // the changes are not identical. 
                                unmergedDocumentChanges.Add(change);

                                groupSessionInfo.OverlappingDistinctDiffs++;
                                if (change.Span == cumulativeChange.Span)
                                {
                                    groupSessionInfo.OverlappingDistinctDiffsWithSameSpan++;
                                    if (change.NewText.Contains(cumulativeChange.NewText) || cumulativeChange.NewText.Contains(change.NewText))
                                    {
                                        groupSessionInfo.OverlappingDistinctDiffsWithSameSpanAndSubstringRelation++;
                                    }
                                }
                            }
                            else
                            {
                                // The current change in consideration is identical to an existing change
                                successfullyMergedChanges.Add(change);
                                cumulativeChangeIndex++;

                                groupSessionInfo.IdenticalDiffs++;
                            }
                        }
                    }
                    else
                    {
                        // The current change in consideration does not intersect with any existing change
                        successfullyMergedChanges.Add(change);

                        groupSessionInfo.IsolatedDiffs++;
                    }
                }

                while (cumulativeChangeIndex < cumulativeChanges.Count)
                {
                    // Existing change that does not overlap with the current change in consideration
                    successfullyMergedChanges.Add(cumulativeChanges[cumulativeChangeIndex]);
                    cumulativeChangeIndex++;
                    groupSessionInfo.IsolatedDiffs++;
                }

                if (unmergedDocumentChanges.Any())
                {
                    unmergedChanges.Add(new UnmergedDocumentChanges(
                        unmergedDocumentChanges.AsEnumerable(),
                        await oldDocument.GetTextAsync(cancellationToken).ConfigureAwait(false),
                        oldDocument.Project.Name));
                }

                return successfullyMergedChanges;
            }
Exemple #7
0
 public void LogLinkedFileResult(LinkedFileGroupSessionInfo info)
 {
     LinkedFileGroups.Add(info);
 }
Exemple #8
0
        private IEnumerable <TextChange> MergeChangesWithMergeFailComments(
            IEnumerable <TextChange> mergedChanges,
            IEnumerable <TextChange> commentChanges,
            IList <TextSpan> mergeConflictResolutionSpans,
            LinkedFileGroupSessionInfo groupSessionInfo)
        {
            var mergedChangesList  = NormalizeChanges(mergedChanges).ToList();
            var commentChangesList = NormalizeChanges(commentChanges).ToList();

            var combinedChanges = new List <TextChange>();
            var insertedMergeConflictCommentsAtAdjustedLocation = 0;

            var commentChangeIndex   = 0;
            var currentPositionDelta = 0;

            foreach (var mergedChange in mergedChangesList)
            {
                while (commentChangeIndex < commentChangesList.Count && commentChangesList[commentChangeIndex].Span.End <= mergedChange.Span.Start)
                {
                    // Add a comment change that does not conflict with any merge change
                    combinedChanges.Add(commentChangesList[commentChangeIndex]);
                    mergeConflictResolutionSpans.Add(new TextSpan(commentChangesList[commentChangeIndex].Span.Start + currentPositionDelta, commentChangesList[commentChangeIndex].NewText.Length));
                    currentPositionDelta += (commentChangesList[commentChangeIndex].NewText.Length - commentChangesList[commentChangeIndex].Span.Length);
                    commentChangeIndex++;
                }

                if (commentChangeIndex >= commentChangesList.Count || mergedChange.Span.End <= commentChangesList[commentChangeIndex].Span.Start)
                {
                    // Add a merge change that does not conflict with any comment change
                    combinedChanges.Add(mergedChange);
                    currentPositionDelta += (mergedChange.NewText.Length - mergedChange.Span.Length);
                    continue;
                }

                // The current comment insertion location conflicts with a merge diff location. Add the comment before the diff.
                var conflictingCommentInsertionLocation = new TextSpan(mergedChange.Span.Start, 0);
                while (commentChangeIndex < commentChangesList.Count && commentChangesList[commentChangeIndex].Span.Start < mergedChange.Span.End)
                {
                    combinedChanges.Add(new TextChange(conflictingCommentInsertionLocation, commentChangesList[commentChangeIndex].NewText));
                    mergeConflictResolutionSpans.Add(new TextSpan(commentChangesList[commentChangeIndex].Span.Start + currentPositionDelta, commentChangesList[commentChangeIndex].NewText.Length));
                    currentPositionDelta += commentChangesList[commentChangeIndex].NewText.Length;

                    commentChangeIndex++;
                    insertedMergeConflictCommentsAtAdjustedLocation++;
                }

                combinedChanges.Add(mergedChange);
                currentPositionDelta += (mergedChange.NewText.Length - mergedChange.Span.Length);
            }

            while (commentChangeIndex < commentChangesList.Count)
            {
                // Add a comment change that does not conflict with any merge change
                combinedChanges.Add(commentChangesList[commentChangeIndex]);
                mergeConflictResolutionSpans.Add(new TextSpan(commentChangesList[commentChangeIndex].Span.Start + currentPositionDelta, commentChangesList[commentChangeIndex].NewText.Length));

                currentPositionDelta += (commentChangesList[commentChangeIndex].NewText.Length - commentChangesList[commentChangeIndex].Span.Length);
                commentChangeIndex++;
            }

            groupSessionInfo.InsertedMergeConflictComments = commentChanges.Count();
            groupSessionInfo.InsertedMergeConflictCommentsAtAdjustedLocation = insertedMergeConflictCommentsAtAdjustedLocation;

            return(NormalizeChanges(combinedChanges));
        }
Exemple #9
0
        private static async Task <ImmutableArray <TextChange> > AddDocumentMergeChangesAsync(
            Document oldDocument,
            Document newDocument,
            List <TextChange> cumulativeChanges,
            List <UnmergedDocumentChanges> unmergedChanges,
            LinkedFileGroupSessionInfo groupSessionInfo,
            IDocumentTextDifferencingService textDiffService,
            CancellationToken cancellationToken)
        {
            var unmergedDocumentChanges   = new List <TextChange>();
            var successfullyMergedChanges = ArrayBuilder <TextChange> .GetInstance();

            int cumulativeChangeIndex = 0;

            var textchanges = await textDiffService.GetTextChangesAsync(oldDocument, newDocument, cancellationToken).ConfigureAwait(false);

            foreach (var change in textchanges)
            {
                while (cumulativeChangeIndex < cumulativeChanges.Count && cumulativeChanges[cumulativeChangeIndex].Span.End < change.Span.Start)
                {
                    // Existing change that does not overlap with the current change in consideration
                    successfullyMergedChanges.Add(cumulativeChanges[cumulativeChangeIndex]);
                    cumulativeChangeIndex++;

                    groupSessionInfo.IsolatedDiffs++;
                }

                if (cumulativeChangeIndex < cumulativeChanges.Count)
                {
                    var cumulativeChange = cumulativeChanges[cumulativeChangeIndex];
                    if (!cumulativeChange.Span.IntersectsWith(change.Span))
                    {
                        // The current change in consideration does not intersect with any existing change
                        successfullyMergedChanges.Add(change);

                        groupSessionInfo.IsolatedDiffs++;
                    }
                    else
                    {
                        if (change.Span != cumulativeChange.Span || change.NewText != cumulativeChange.NewText)
                        {
                            // The current change in consideration overlaps an existing change but
                            // the changes are not identical.
                            unmergedDocumentChanges.Add(change);

                            groupSessionInfo.OverlappingDistinctDiffs++;
                            if (change.Span == cumulativeChange.Span)
                            {
                                groupSessionInfo.OverlappingDistinctDiffsWithSameSpan++;
                                if (change.NewText.Contains(cumulativeChange.NewText) || cumulativeChange.NewText.Contains(change.NewText))
                                {
                                    groupSessionInfo.OverlappingDistinctDiffsWithSameSpanAndSubstringRelation++;
                                }
                            }
                        }
                        else
                        {
                            // The current change in consideration is identical to an existing change
                            successfullyMergedChanges.Add(change);
                            cumulativeChangeIndex++;

                            groupSessionInfo.IdenticalDiffs++;
                        }
                    }
                }
                else
                {
                    // The current change in consideration does not intersect with any existing change
                    successfullyMergedChanges.Add(change);

                    groupSessionInfo.IsolatedDiffs++;
                }
            }

            while (cumulativeChangeIndex < cumulativeChanges.Count)
            {
                // Existing change that does not overlap with the current change in consideration
                successfullyMergedChanges.Add(cumulativeChanges[cumulativeChangeIndex]);
                cumulativeChangeIndex++;
                groupSessionInfo.IsolatedDiffs++;
            }

            if (unmergedDocumentChanges.Any())
            {
                unmergedChanges.Add(new UnmergedDocumentChanges(
                                        unmergedDocumentChanges.AsEnumerable(),
                                        oldDocument.Project.Name,
                                        oldDocument.Id));
            }

            return(successfullyMergedChanges.ToImmutableAndFree());
        }