|  | 
|  | 1 | +// Copyright (c) Microsoft.  All Rights Reserved.  Licensed under the Apache License, Version 2.0.  See License.txt in the project root for license information. | 
|  | 2 | + | 
|  | 3 | +using System; | 
|  | 4 | +using System.Collections.Generic; | 
|  | 5 | +using System.Diagnostics; | 
|  | 6 | +using System.IO; | 
|  | 7 | + | 
|  | 8 | +namespace Microsoft.Build.Tasks.Git | 
|  | 9 | +{ | 
|  | 10 | +    partial class GitIgnore | 
|  | 11 | +    { | 
|  | 12 | +        internal sealed class Matcher | 
|  | 13 | +        { | 
|  | 14 | +            public GitIgnore Ignore { get; } | 
|  | 15 | + | 
|  | 16 | +            /// <summary> | 
|  | 17 | +            /// Maps full posix slash-terminated directory name to a pattern group. | 
|  | 18 | +            /// </summary> | 
|  | 19 | +            private readonly Dictionary<string, PatternGroup> _patternGroups; | 
|  | 20 | + | 
|  | 21 | +            /// <summary> | 
|  | 22 | +            /// The result of "is ignored" for directories. | 
|  | 23 | +            /// </summary> | 
|  | 24 | +            private readonly Dictionary<string, bool> _directoryIgnoreStateCache; | 
|  | 25 | + | 
|  | 26 | +            private readonly List<PatternGroup> _reusableGroupList; | 
|  | 27 | + | 
|  | 28 | +            internal Matcher(GitIgnore ignore) | 
|  | 29 | +            { | 
|  | 30 | +                Ignore = ignore; | 
|  | 31 | +                _patternGroups = new Dictionary<string, PatternGroup>(); | 
|  | 32 | +                _directoryIgnoreStateCache = new Dictionary<string, bool>(Ignore.PathComparer); | 
|  | 33 | +                _reusableGroupList = new List<PatternGroup>(); | 
|  | 34 | +            } | 
|  | 35 | + | 
|  | 36 | +            // test only: | 
|  | 37 | +            internal IReadOnlyDictionary<string, bool> DirectoryIgnoreStateCache | 
|  | 38 | +                => _directoryIgnoreStateCache; | 
|  | 39 | + | 
|  | 40 | +            private PatternGroup GetPatternGroup(string directory) | 
|  | 41 | +            { | 
|  | 42 | +                if (_patternGroups.TryGetValue(directory, out var group)) | 
|  | 43 | +                { | 
|  | 44 | +                    return group; | 
|  | 45 | +                } | 
|  | 46 | + | 
|  | 47 | +                PatternGroup parent; | 
|  | 48 | +                if (directory.Equals(Ignore.WorkingDirectory, Ignore.PathComparison)) | 
|  | 49 | +                { | 
|  | 50 | +                    parent = Ignore.Root; | 
|  | 51 | +                } | 
|  | 52 | +                else | 
|  | 53 | +                { | 
|  | 54 | +                    parent = GetPatternGroup(PathUtils.ToPosixDirectoryPath(Path.GetDirectoryName(PathUtils.TrimTrailingSlash(directory)))); | 
|  | 55 | +                } | 
|  | 56 | + | 
|  | 57 | +                group = LoadFromFile(Path.Combine(directory, GitIgnoreFileName), parent) ?? parent; | 
|  | 58 | + | 
|  | 59 | +                _patternGroups.Add(directory, group); | 
|  | 60 | +                return group; | 
|  | 61 | +            } | 
|  | 62 | + | 
|  | 63 | +            /// <summary> | 
|  | 64 | +            /// Checks if the specified file path is ignored. | 
|  | 65 | +            /// </summary> | 
|  | 66 | +            /// <param name="fullPath">Normalized path.</param> | 
|  | 67 | +            /// <returns>True if the path is ignored, fale if it is not, null if it is outside of the working directory.</returns> | 
|  | 68 | +            public bool? IsNormalizedFilePathIgnored(string fullPath) | 
|  | 69 | +            { | 
|  | 70 | +                if (!PathUtils.IsAbsolute(fullPath)) | 
|  | 71 | +                { | 
|  | 72 | +                    throw new ArgumentException("Path must be absolute", nameof(fullPath)); | 
|  | 73 | +                } | 
|  | 74 | + | 
|  | 75 | +                if (PathUtils.HasTrailingDirectorySeparator(fullPath)) | 
|  | 76 | +                { | 
|  | 77 | +                    throw new ArgumentException("Path must be a file path", nameof(fullPath)); | 
|  | 78 | +                } | 
|  | 79 | + | 
|  | 80 | +                return IsPathIgnored(PathUtils.ToPosixPath(fullPath), isDirectoryPath: false); | 
|  | 81 | +            } | 
|  | 82 | + | 
|  | 83 | +            /// <summary> | 
|  | 84 | +            /// Checks if the specified path is ignored. | 
|  | 85 | +            /// </summary> | 
|  | 86 | +            /// <param name="fullPath">Full path.</param> | 
|  | 87 | +            /// <returns>True if the path is ignored, fale if it is not, null if it is outside of the working directory.</returns> | 
|  | 88 | +            public bool? IsPathIgnored(string fullPath) | 
|  | 89 | +            { | 
|  | 90 | +                if (!PathUtils.IsAbsolute(fullPath)) | 
|  | 91 | +                { | 
|  | 92 | +                    throw new ArgumentException("Path must be absolute", nameof(fullPath)); | 
|  | 93 | +                } | 
|  | 94 | + | 
|  | 95 | +                // git uses the FS case-sensitivity for checking directory existence: | 
|  | 96 | +                bool isDirectoryPath = PathUtils.HasTrailingDirectorySeparator(fullPath) || Directory.Exists(fullPath); | 
|  | 97 | + | 
|  | 98 | +                var fullPathNoSlash = PathUtils.TrimTrailingSlash(PathUtils.ToPosixPath(Path.GetFullPath(fullPath))); | 
|  | 99 | +                if (isDirectoryPath && fullPathNoSlash.Equals(Ignore._workingDirectoryNoSlash, Ignore.PathComparison)) | 
|  | 100 | +                { | 
|  | 101 | +                    return false; | 
|  | 102 | +                } | 
|  | 103 | + | 
|  | 104 | +                return IsPathIgnored(fullPathNoSlash, isDirectoryPath); | 
|  | 105 | +            } | 
|  | 106 | + | 
|  | 107 | +            private bool? IsPathIgnored(string normalizedPosixPath, bool isDirectoryPath) | 
|  | 108 | +            { | 
|  | 109 | +                Debug.Assert(PathUtils.IsAbsolute(normalizedPosixPath)); | 
|  | 110 | +                Debug.Assert(PathUtils.IsPosixPath(normalizedPosixPath)); | 
|  | 111 | +                Debug.Assert(!PathUtils.HasTrailingSlash(normalizedPosixPath)); | 
|  | 112 | + | 
|  | 113 | +                // paths outside of working directory: | 
|  | 114 | +                if (!normalizedPosixPath.StartsWith(Ignore.WorkingDirectory, Ignore.PathComparison)) | 
|  | 115 | +                { | 
|  | 116 | +                    return null; | 
|  | 117 | +                } | 
|  | 118 | + | 
|  | 119 | +                if (isDirectoryPath && _directoryIgnoreStateCache.TryGetValue(normalizedPosixPath, out var isIgnored)) | 
|  | 120 | +                { | 
|  | 121 | +                    return isIgnored; | 
|  | 122 | +                } | 
|  | 123 | + | 
|  | 124 | +                isIgnored = IsIgnoredRecursive(normalizedPosixPath, isDirectoryPath); | 
|  | 125 | +                if (isDirectoryPath) | 
|  | 126 | +                { | 
|  | 127 | +                    _directoryIgnoreStateCache.Add(normalizedPosixPath, isIgnored); | 
|  | 128 | +                } | 
|  | 129 | + | 
|  | 130 | +                return isIgnored; | 
|  | 131 | +            } | 
|  | 132 | + | 
|  | 133 | +            private bool IsIgnoredRecursive(string normalizedPosixPath, bool isDirectoryPath) | 
|  | 134 | +            { | 
|  | 135 | +                SplitPath(normalizedPosixPath, out var directory, out var fileName); | 
|  | 136 | +                if (directory == null || !directory.StartsWith(Ignore.WorkingDirectory, Ignore.PathComparison)) | 
|  | 137 | +                { | 
|  | 138 | +                    return false; | 
|  | 139 | +                } | 
|  | 140 | + | 
|  | 141 | +                var isIgnored = IsIgnored(normalizedPosixPath, directory, fileName, isDirectoryPath); | 
|  | 142 | +                if (isIgnored) | 
|  | 143 | +                { | 
|  | 144 | +                    return true; | 
|  | 145 | +                } | 
|  | 146 | + | 
|  | 147 | +                // The target file/directory itself is not ignored, but its containing directory might be. | 
|  | 148 | +                normalizedPosixPath = PathUtils.TrimTrailingSlash(directory); | 
|  | 149 | +                if (_directoryIgnoreStateCache.TryGetValue(normalizedPosixPath, out isIgnored)) | 
|  | 150 | +                { | 
|  | 151 | +                    return isIgnored; | 
|  | 152 | +                } | 
|  | 153 | + | 
|  | 154 | +                isIgnored = IsIgnoredRecursive(normalizedPosixPath, isDirectoryPath: true); | 
|  | 155 | +                _directoryIgnoreStateCache.Add(normalizedPosixPath, isIgnored); | 
|  | 156 | +                return isIgnored; | 
|  | 157 | +            } | 
|  | 158 | + | 
|  | 159 | +            private static void SplitPath(string fullPath, out string directoryWithSlash, out string fileName) | 
|  | 160 | +            { | 
|  | 161 | +                Debug.Assert(!PathUtils.HasTrailingSlash(fullPath)); | 
|  | 162 | +                int i = fullPath.LastIndexOf('/'); | 
|  | 163 | +                if (i < 0) | 
|  | 164 | +                { | 
|  | 165 | +                    directoryWithSlash = null; | 
|  | 166 | +                    fileName = fullPath; | 
|  | 167 | +                } | 
|  | 168 | +                else | 
|  | 169 | +                { | 
|  | 170 | +                    directoryWithSlash = fullPath.Substring(0, i + 1); | 
|  | 171 | +                    fileName = fullPath.Substring(i + 1); | 
|  | 172 | +                } | 
|  | 173 | +            } | 
|  | 174 | + | 
|  | 175 | +            private bool IsIgnored(string normalizedPosixPath, string directory, string fileName, bool isDirectoryPath) | 
|  | 176 | +            { | 
|  | 177 | +                // Default patterns can't be overriden by a negative pattern: | 
|  | 178 | +                if (fileName.Equals(".git", Ignore.PathComparison)) | 
|  | 179 | +                { | 
|  | 180 | +                    return true; | 
|  | 181 | +                } | 
|  | 182 | + | 
|  | 183 | +                bool isIgnored = false; | 
|  | 184 | +                 | 
|  | 185 | +                // Visit groups in reverse order. | 
|  | 186 | +                // Patterns specified closer to the target file override those specified above. | 
|  | 187 | +                _reusableGroupList.Clear(); | 
|  | 188 | +                var groups = _reusableGroupList; | 
|  | 189 | +                for (var patternGroup = GetPatternGroup(directory); patternGroup != null; patternGroup = patternGroup.Parent) | 
|  | 190 | +                { | 
|  | 191 | +                    groups.Add(patternGroup); | 
|  | 192 | +                } | 
|  | 193 | + | 
|  | 194 | +                for (int i = groups.Count - 1; i >= 0; i--) | 
|  | 195 | +                { | 
|  | 196 | +                    var patternGroup = groups[i]; | 
|  | 197 | + | 
|  | 198 | +                    if (!normalizedPosixPath.StartsWith(patternGroup.ContainingDirectory, Ignore.PathComparison)) | 
|  | 199 | +                    { | 
|  | 200 | +                        continue; | 
|  | 201 | +                    } | 
|  | 202 | + | 
|  | 203 | +                    string lazyRelativePath = null; | 
|  | 204 | + | 
|  | 205 | +                    foreach (var pattern in patternGroup.Patterns) | 
|  | 206 | +                    { | 
|  | 207 | +                        // If a pattern is matched as ignored only look for a negative pattern that matches as well. | 
|  | 208 | +                        // If a pattern is not matched then skip negative patterns. | 
|  | 209 | +                        if (isIgnored != pattern.IsNegative) | 
|  | 210 | +                        { | 
|  | 211 | +                            continue; | 
|  | 212 | +                        } | 
|  | 213 | + | 
|  | 214 | +                        if (pattern.IsDirectoryPattern && !isDirectoryPath) | 
|  | 215 | +                        { | 
|  | 216 | +                            continue; | 
|  | 217 | +                        } | 
|  | 218 | + | 
|  | 219 | +                        string matchPath = pattern.IsFullPathPattern ? | 
|  | 220 | +                            lazyRelativePath ??= normalizedPosixPath.Substring(patternGroup.ContainingDirectory.Length) : | 
|  | 221 | +                            fileName; | 
|  | 222 | + | 
|  | 223 | +                        if (Glob.IsMatch(pattern.Glob, matchPath, Ignore.IgnoreCase, matchWildCardWithDirectorySeparator: false)) | 
|  | 224 | +                        { | 
|  | 225 | +                            // TODO: optimize negative pattern lookup (once we match, do we need to continue matching?) | 
|  | 226 | +                            isIgnored = !pattern.IsNegative; | 
|  | 227 | +                        } | 
|  | 228 | +                    } | 
|  | 229 | +                } | 
|  | 230 | + | 
|  | 231 | +                return isIgnored; | 
|  | 232 | +            } | 
|  | 233 | +        } | 
|  | 234 | +    } | 
|  | 235 | +} | 
0 commit comments