private async Task GetFormattedDiff(RenderedCodeFile renderedCodeFile, ReviewRevisionModel lastRevision, StringBuilder stringBuilder) { RenderedCodeFile autoReview = await _codeFileRepository.GetCodeFileAsync(lastRevision, false); var autoReviewTextFile = autoReview.RenderText(showDocumentation: false, skipDiff: true); var prCodeTextFile = renderedCodeFile.RenderText(showDocumentation: false, skipDiff: true); var diffLines = InlineDiff.Compute(autoReviewTextFile, prCodeTextFile, autoReviewTextFile, prCodeTextFile); if (diffLines == null || diffLines.Length == 0 || diffLines.Count(l => l.Kind != DiffLineKind.Unchanged) > 10) { return; } stringBuilder.Append(Environment.NewLine).Append("**API changes**").Append(Environment.NewLine); stringBuilder.Append("```diff").Append(Environment.NewLine); foreach (var line in diffLines) { if (line.Kind == DiffLineKind.Added) { stringBuilder.Append("+ ").Append(line.Line.DisplayString).Append(Environment.NewLine); } else if (line.Kind == DiffLineKind.Removed) { stringBuilder.Append("- ").Append(line.Line.DisplayString).Append(Environment.NewLine); } } stringBuilder.Append("```"); }
public async Task <string> GetApiDiffFromAutomaticReview(CodeFile codeFile, int prNumber, string originalFileName, MemoryStream memoryStream, PullRequestModel pullRequestModel) { // Get automatically generated master review for package or previously cloned review for this pull request var review = await GetBaseLineReview(codeFile.Language, codeFile.PackageName, pullRequestModel); if (review == null) { return(""); } // Check if API surface level matches with any revisions var renderedCodeFile = new RenderedCodeFile(codeFile); if (await IsReviewSame(review, renderedCodeFile)) { return(""); } if (pullRequestModel.ReviewId != null) { // If baseline review was already created and if APIs in current commit doesn't match any of the revisions in generated review then create new baseline using main branch and compare again. // If APIs are still different, find the diff against latest baseline. review = await GetBaseLineReview(codeFile.Language, codeFile.PackageName, pullRequestModel, true); review.ReviewId = pullRequestModel.ReviewId; if (await IsReviewSame(review, renderedCodeFile)) { return(""); } } var newRevision = new ReviewRevisionModel() { Author = review.Author, Label = "Created for PR " + prNumber }; var stringBuilder = new StringBuilder(); var diffUrl = REVIEW_DIFF_URL.Replace("{ReviewId}", review.ReviewId).Replace("{NewRevision}", review.Revisions.Last().RevisionId); stringBuilder.Append($"API changes have been detected in `{codeFile.PackageName}`. You can review API changes [here]({diffUrl})").Append(Environment.NewLine); // If review doesn't match with any revisions then generate formatted diff against last revision of automatic review await GetFormattedDiff(renderedCodeFile, review.Revisions.Last(), stringBuilder); var reviewCodeFileModel = await _reviewManager.CreateReviewCodeFileModel(newRevision.RevisionId, memoryStream, codeFile); reviewCodeFileModel.FileName = originalFileName; newRevision.Files.Add(reviewCodeFileModel); review.Revisions.Add(newRevision); pullRequestModel.ReviewId = review.ReviewId; review.FilterType = ReviewType.PullRequest; await _reviewsRepository.UpsertReviewAsync(review); await _pullRequestsRepository.UpsertPullRequestAsync(pullRequestModel); return(stringBuilder.ToString()); }
private async Task <bool> IsReviewSame(ReviewRevisionModel revision, RenderedCodeFile renderedCodeFile) { //This will compare and check if new code file content is same as revision in parameter var lastRevisionFile = await _codeFileRepository.GetCodeFileAsync(revision); var lastRevisionTextLines = lastRevisionFile.RenderText(showDocumentation: false, skipDiff: true); var fileTextLines = renderedCodeFile.RenderText(showDocumentation: false, skipDiff: true); return(lastRevisionTextLines.SequenceEqual(fileTextLines)); }
private async Task <bool> IsReviewSame(ReviewModel review, RenderedCodeFile renderedCodeFile) { foreach (var revision in review.Revisions.Reverse()) { if (await _reviewManager.IsReviewSame(revision, renderedCodeFile)) { return(true); } } return(false); }
private async Task <bool> IsReviewSame(ReviewModel review, CodeFile newCodeFile) { //This will compare and check if new code file content is same as last revision of given review in parameter //Get latest revision of review and check diff var lastRevisionFile = await _codeFileRepository.GetCodeFileAsync(review.Revisions.Last()); var lastRevisionTextLines = lastRevisionFile.RenderText(false); var renderedCodeFile = new RenderedCodeFile(newCodeFile); var fileTextLines = renderedCodeFile.RenderText(false); return(lastRevisionTextLines.SequenceEqual(fileTextLines)); }
public async Task <RenderedCodeFile> GetCodeFileAsync(string revisionId, string codeFileId) { var client = GetBlobClient(revisionId, codeFileId, out var key); if (_cache.TryGetValue <RenderedCodeFile>(key, out var codeFile)) { return(codeFile); } var info = await client.DownloadAsync(); codeFile = new RenderedCodeFile(await CodeFile.DeserializeAsync(info.Value.Content)); using var _ = _cache.CreateEntry(key) .SetSlidingExpiration(TimeSpan.FromDays(1)) .SetValue(codeFile); return(codeFile); }
public async Task <ReviewModel> CreateMasterReviewAsync(ClaimsPrincipal user, string originalName, string label, Stream fileStream, bool runAnalysis) { //Generate code file from new uploaded package using var memoryStream = new MemoryStream(); var codeFile = await CreateCodeFile(originalName, fileStream, runAnalysis, memoryStream); //Get current master review for package and language var review = await _reviewsRepository.GetMasterReviewForPackageAsync(codeFile.Language, codeFile.PackageName); bool createNewRevision = true; if (review != null) { // Delete pending revisions if it is not in approved state before adding new revision // This is to keep only one pending revision since last approval or from initial review revision var lastRevision = review.Revisions.LastOrDefault(); while (lastRevision.Approvers.Count == 0 && review.Revisions.Count > 1) { review.Revisions.Remove(lastRevision); lastRevision = review.Revisions.LastOrDefault(); } var renderedCodeFile = new RenderedCodeFile(codeFile); var noDiffFound = await IsReviewSame(review.Revisions.LastOrDefault(), renderedCodeFile); if (noDiffFound) { // No change is detected from last revision so no need to update this revision createNewRevision = false; } } else { // Package and language combination doesn't have automatically created review. Create a new review. review = new ReviewModel { Author = user.GetGitHubLogin(), CreationDate = DateTime.UtcNow, RunAnalysis = runAnalysis, Name = originalName, IsAutomatic = true }; } // Check if user is authorized to modify automatic review await AssertAutomaticReviewModifier(user, review); if (createNewRevision) { // Update or insert review with new revision var revision = new ReviewRevisionModel() { Author = user.GetGitHubLogin(), Label = label }; var reviewCodeFileModel = await CreateReviewCodeFileModel(revision.RevisionId, memoryStream, codeFile); reviewCodeFileModel.FileName = originalName; revision.Files.Add(reviewCodeFileModel); review.Revisions.Add(revision); } // Check if review can be marked as approved if another review with same surface level is in approved status if (review.Revisions.Last().Approvers.Count() == 0) { var matchingApprovedRevision = await FindMatchingApprovedRevision(review); if (matchingApprovedRevision != null) { foreach (var approver in matchingApprovedRevision.Approvers) { review.Revisions.Last().Approvers.Add(approver); } } } await _reviewsRepository.UpsertReviewAsync(review); return(review); }
private async Task <ReviewRevisionModel> CreateMasterReviewAsync(ClaimsPrincipal user, CodeFile codeFile, string originalName, string label, MemoryStream memoryStream, bool compareAllRevisions) { var renderedCodeFile = new RenderedCodeFile(codeFile); //Get current master review for package and language var review = await _reviewsRepository.GetMasterReviewForPackageAsync(codeFile.Language, codeFile.PackageName); bool createNewRevision = true; ReviewRevisionModel reviewRevision = null; if (review != null) { // Delete pending revisions if it is not in approved state and if it doesn't have any comments before adding new revision // This is to keep only one pending revision since last approval or from initial review revision var lastRevision = review.Revisions.LastOrDefault(); var comments = await _commentsRepository.GetCommentsAsync(review.ReviewId); while (lastRevision.Approvers.Count == 0 && review.Revisions.Count > 1 && !await IsReviewSame(lastRevision, renderedCodeFile) && !comments.Any(c => lastRevision.RevisionId == c.RevisionId)) { review.Revisions.Remove(lastRevision); lastRevision = review.Revisions.LastOrDefault(); } // We should compare against only latest revision when calling this API from scheduled CI runs // But any manual pipeline run at release time should compare against all approved revisions to ensure hotfix release doesn't have API change // If review surface doesn't match with any approved revisions then we will create new revision if it doesn't match pending latest revision if (compareAllRevisions) { foreach (var approvedRevision in review.Revisions.Where(r => r.IsApproved).Reverse()) { if (await IsReviewSame(approvedRevision, renderedCodeFile)) { return(approvedRevision); } } } if (await IsReviewSame(lastRevision, renderedCodeFile)) { reviewRevision = lastRevision; createNewRevision = false; } } else { // Package and language combination doesn't have automatically created review. Create a new review. review = new ReviewModel { Author = user.GetGitHubLogin(), CreationDate = DateTime.UtcNow, RunAnalysis = false, Name = originalName, FilterType = ReviewType.Automatic }; } // Check if user is authorized to modify automatic review await AssertAutomaticReviewModifier(user, review); if (createNewRevision) { // Update or insert review with new revision reviewRevision = new ReviewRevisionModel() { Author = user.GetGitHubLogin(), Label = label }; var reviewCodeFileModel = await CreateReviewCodeFileModel(reviewRevision.RevisionId, memoryStream, codeFile); reviewCodeFileModel.FileName = originalName; reviewRevision.Files.Add(reviewCodeFileModel); review.Revisions.Add(reviewRevision); } // Check if review can be marked as approved if another review with same surface level is in approved status if (review.Revisions.Last().Approvers.Count() == 0) { var matchingApprovedRevision = await FindMatchingApprovedRevision(review); if (matchingApprovedRevision != null) { foreach (var approver in matchingApprovedRevision.Approvers) { review.Revisions.Last().Approvers.Add(approver); } } } await _reviewsRepository.UpsertReviewAsync(review); return(reviewRevision); }
private async Task CreateRevisionIfRequired(CodeFile codeFile, int prNumber, string originalFileName, MemoryStream memoryStream, PullRequestModel pullRequestModel, CodeFile baselineCodeFile, MemoryStream baseLineStream, string baselineFileName) { var newRevision = new ReviewRevisionModel() { Author = pullRequestModel.Author, Label = $"Created for PR {prNumber}" }; // Get automatically generated master review for package or previously cloned review for this pull request var review = await GetBaseLineReview(codeFile.Language, codeFile.PackageName, pullRequestModel); if (review == null) { // If base line is not available (possible if package is new or request coming from SDK automation) review = CreateNewReview(pullRequestModel); // If request passes code file for baseline if (baselineCodeFile != null) { var baseline = await CreateBaselineRevision(baselineCodeFile, baseLineStream, pullRequestModel, baselineFileName); review.Revisions.Add(baseline); } } else { // Check if API surface level matches with any revisions var renderedCodeFile = new RenderedCodeFile(codeFile); if (await IsReviewSame(review, renderedCodeFile)) { return; } if (pullRequestModel.ReviewId != null) { //Refresh baseline using latest from automatic review var prevRevisionId = review.Revisions.Last().RevisionId; review = await GetBaseLineReview(codeFile.Language, codeFile.PackageName, pullRequestModel, true); review.ReviewId = pullRequestModel.ReviewId; newRevision.RevisionId = prevRevisionId; } } var reviewCodeFileModel = await _reviewManager.CreateReviewCodeFileModel(newRevision.RevisionId, memoryStream, codeFile); reviewCodeFileModel.FileName = originalFileName; newRevision.Files.Add(reviewCodeFileModel); review.Revisions.Add(newRevision); pullRequestModel.ReviewId = review.ReviewId; review.FilterType = ReviewType.PullRequest; await _reviewsRepository.UpsertReviewAsync(review); await _pullRequestsRepository.UpsertPullRequestAsync(pullRequestModel); }