|
| 1 | +// Licensed to the .NET Foundation under one or more agreements. |
| 2 | +// The .NET Foundation licenses this file to you under the MIT license. |
| 3 | + |
| 4 | +using Microsoft.Extensions.Logging; |
| 5 | +using Microsoft.Extensions.Logging.Abstractions; |
| 6 | +using Microsoft.Extensions.FileSystemGlobbing; |
| 7 | +using Microsoft.Extensions.FileSystemGlobbing.Abstractions; |
| 8 | +using Microsoft.DotNet.DarcLib; |
| 9 | +using Microsoft.DotNet.DarcLib.VirtualMonoRepo; |
| 10 | +using Microsoft.DotNet.DarcLib.Models.VirtualMonoRepo; |
| 11 | +using System.Linq; |
| 12 | +using System.Collections.Generic; |
| 13 | +using System.Text; |
| 14 | +using Newtonsoft.Json.Linq; |
| 15 | +using Newtonsoft.Json; |
| 16 | +using Microsoft.DotNet.DarcLib.Helpers; |
| 17 | +using static ChangeValidation.Validation; |
| 18 | + |
| 19 | +namespace ChangeValidation; |
| 20 | + |
| 21 | +internal class ExclusionFileValidation : IValidationStep |
| 22 | +{ |
| 23 | + private static readonly int maxDisplayedFiles = 20; |
| 24 | + private readonly IVmrDependencyTracker _dependencyTracker; |
| 25 | + private readonly IProcessManager _processManager; |
| 26 | + private readonly IVmrInfo _vmrInfo; |
| 27 | + private readonly ILocalGitRepoFactory _localGitRepoFactory; |
| 28 | + |
| 29 | + public ExclusionFileValidation( |
| 30 | + IVmrDependencyTracker dependencyTracker, |
| 31 | + IProcessManager processManager, |
| 32 | + ILocalGitRepoFactory localGitRepoFactory, |
| 33 | + IVmrInfo vmrInfo) |
| 34 | + { |
| 35 | + _dependencyTracker = dependencyTracker; |
| 36 | + _processManager = processManager; |
| 37 | + _localGitRepoFactory = localGitRepoFactory; |
| 38 | + _vmrInfo = vmrInfo; |
| 39 | + } |
| 40 | + |
| 41 | + public string DisplayName => "Exclusion File Validation"; |
| 42 | + |
| 43 | + public async Task<bool> Validate(PrInfo prInfo) |
| 44 | + { |
| 45 | + ILocalGitRepo vmr = _localGitRepoFactory.Create(_vmrInfo.VmrPath); |
| 46 | + |
| 47 | + var newExclusionRules = await GetExclusionPatterns(); // We are already checked to the PR Head commit, just parse exclusions from file |
| 48 | + var originalExclusionRules = await GetExclusionPatternsFromBranch(vmr, prInfo.TargetBranch); |
| 49 | + |
| 50 | + var newExcludedFiles = FindMatchingFiles(newExclusionRules); |
| 51 | + var originalExcludedFiles = FindMatchingFiles(originalExclusionRules); |
| 52 | + |
| 53 | + var excludedFilesInPr = prInfo.ChangedFiles |
| 54 | + .Where(file => newExcludedFiles.Contains(NormalizePath(file))) |
| 55 | + .ToList(); |
| 56 | + |
| 57 | + var filesMatchingNewExclusionRules = newExcludedFiles |
| 58 | + .Where(file => !originalExcludedFiles.Contains(NormalizePath(file)) && !prInfo.ChangedFiles.Contains(NormalizePath(file))) |
| 59 | + .ToList(); |
| 60 | + |
| 61 | + foreach (var file in excludedFilesInPr.Take(maxDisplayedFiles).ToList()) |
| 62 | + { |
| 63 | + LogError($"This PR modifies the file `{file}`, which is part of the excluded files" + |
| 64 | + $" defined in {VmrInfo.DefaultRelativeSourceMappingsPath}. If these changes are" + |
| 65 | + $" necessary, please contact @dotnet/product-construction."); |
| 66 | + } |
| 67 | + |
| 68 | + if (excludedFilesInPr.Count > maxDisplayedFiles) |
| 69 | + { |
| 70 | + LogError($"... {excludedFilesInPr.Count - maxDisplayedFiles} more excluded files detected" + |
| 71 | + $" in the PR (only showing the first {maxDisplayedFiles} files)."); |
| 72 | + } |
| 73 | + |
| 74 | + foreach (var file in filesMatchingNewExclusionRules.Take(maxDisplayedFiles).ToList()) |
| 75 | + { |
| 76 | + LogError($"The new exclusion rules defined in {VmrInfo.DefaultRelativeSourceMappingsPath} " + |
| 77 | + $"include the VMR file `{file}`. If this file is not needed in the VMR, consider " + |
| 78 | + $"deleting it as part of the PR."); |
| 79 | + } |
| 80 | + |
| 81 | + if (filesMatchingNewExclusionRules.Count > maxDisplayedFiles) |
| 82 | + { |
| 83 | + LogError($"... {filesMatchingNewExclusionRules.Count - maxDisplayedFiles} more VMR files " + |
| 84 | + $"match the new exclusion rules (only showing the first {maxDisplayedFiles} files). " + |
| 85 | + "Please review the modifications made to exclusion rules"); |
| 86 | + } |
| 87 | + |
| 88 | + return !excludedFilesInPr.Any() && !filesMatchingNewExclusionRules.Any(); |
| 89 | + } |
| 90 | + |
| 91 | + private async Task<List<string>> GetExclusionPatternsFromBranch(ILocalGitRepo vmr, string branchName) |
| 92 | + { |
| 93 | + string originalRef = await vmr.GetCheckedOutBranchAsync(); |
| 94 | + try |
| 95 | + { |
| 96 | + LogInfo($"Checking out branch {branchName} to get exclusion patterns."); |
| 97 | + await vmr.CheckoutAsync(branchName); |
| 98 | + return await GetExclusionPatterns(); |
| 99 | + } |
| 100 | + finally |
| 101 | + { |
| 102 | + await vmr.CheckoutAsync(originalRef); |
| 103 | + } |
| 104 | + } |
| 105 | + |
| 106 | + private async Task<List<string>> GetExclusionPatterns() |
| 107 | + { |
| 108 | + await _dependencyTracker.RefreshMetadata(); |
| 109 | + |
| 110 | + var sourceMappings = _dependencyTracker.Mappings; |
| 111 | + |
| 112 | + var exclusionPatterns = sourceMappings.SelectMany(mapping => GetVmrGlobFromSourceMapping(mapping)) |
| 113 | + .Distinct() |
| 114 | + .ToList(); |
| 115 | + |
| 116 | + LogInfo("Successfully parsed exclusion patterns..."); |
| 117 | + LogInfo(string.Join(Environment.NewLine, exclusionPatterns)); |
| 118 | + |
| 119 | + return exclusionPatterns; |
| 120 | + } |
| 121 | + |
| 122 | + private List<string> GetVmrGlobFromSourceMapping(SourceMapping mapping) |
| 123 | + { |
| 124 | + return mapping.Exclude.Select(exclusion => "src/" + mapping.Name + "/" + NormalizePath(exclusion)).ToList(); |
| 125 | + } |
| 126 | + |
| 127 | + private HashSet<string> FindMatchingFiles(List<string> globPatterns) |
| 128 | + { |
| 129 | + var matcher = new Matcher(); |
| 130 | + |
| 131 | + foreach (var pattern in globPatterns) |
| 132 | + { |
| 133 | + matcher.AddInclude(pattern); |
| 134 | + } |
| 135 | + |
| 136 | + var directoryInfo = new DirectoryInfo(_vmrInfo.VmrPath); |
| 137 | + var directoryWrapper = new DirectoryInfoWrapper(directoryInfo); |
| 138 | + var result = matcher.Execute(directoryWrapper); |
| 139 | + |
| 140 | + var res = result.Files |
| 141 | + .Select(file => file.Path) |
| 142 | + .ToHashSet(); |
| 143 | + |
| 144 | + return res; |
| 145 | + } |
| 146 | + |
| 147 | + internal static string NormalizePath(string path) |
| 148 | + { |
| 149 | + return path.Replace('\\', '/'); |
| 150 | + } |
| 151 | +} |
0 commit comments