| File: GitDataReader\GitIgnore.Matcher.cs | Web Access |
| Project: src\sourcelink\src\Microsoft.Build.Tasks.Git\Microsoft.Build.Tasks.Git.csproj (Microsoft.Build.Tasks.Git) |
// Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. // See the License.txt file in the project root for more information. using System; using System.Collections.Generic; using System.Diagnostics; using System.IO; namespace Microsoft.Build.Tasks.Git { partial class GitIgnore { internal sealed class Matcher { public GitIgnore Ignore { get; } /// <summary> /// Maps full posix slash-terminated directory name to a pattern group. /// </summary> private readonly Dictionary<string, PatternGroup?> _patternGroups; /// <summary> /// The result of "is ignored" for directories. /// </summary> private readonly Dictionary<string, bool> _directoryIgnoreStateCache; private readonly List<PatternGroup> _reusableGroupList; internal Matcher(GitIgnore ignore) { Ignore = ignore; _patternGroups = new Dictionary<string, PatternGroup?>(StringComparer.Ordinal); _directoryIgnoreStateCache = new Dictionary<string, bool>(Ignore.PathComparer); _reusableGroupList = new List<PatternGroup>(); } // test only: internal IReadOnlyDictionary<string, bool> DirectoryIgnoreStateCache => _directoryIgnoreStateCache; private PatternGroup? GetPatternGroup(string directory) { Debug.Assert(PathUtils.HasTrailingSlash(directory)); if (_patternGroups.TryGetValue(directory, out var group)) { return group; } PatternGroup? parent; if (directory.Equals(Ignore.WorkingDirectory, Ignore.PathComparison)) { parent = Ignore.Root; } else { var parentDirectory = directory[..(directory.LastIndexOf('/', directory.Length - 2, directory.Length - 1) + 1)]; parent = GetPatternGroup(parentDirectory); } group = LoadFromFile(directory + GitIgnoreFileName, parent) ?? parent; _patternGroups.Add(directory, group); return group; } /// <summary> /// Checks if the specified file path is ignored. /// </summary> /// <param name="fullPath">Normalized path.</param> /// <returns>True if the path is ignored, fale if it is not, null if it is outside of the working directory.</returns> public bool? IsNormalizedFilePathIgnored(string fullPath) { if (!PathUtils.IsAbsolute(fullPath)) { throw new ArgumentException(Resources.PathMustBeAbsolute, nameof(fullPath)); } if (PathUtils.HasTrailingDirectorySeparator(fullPath)) { throw new ArgumentException(Resources.PathMustBeFilePath, nameof(fullPath)); } return IsPathIgnored(PathUtils.ToPosixPath(fullPath), isDirectoryPath: false); } /// <summary> /// Checks if the specified path is ignored. /// </summary> /// <param name="fullPath">Full path.</param> /// <returns>True if the path is ignored, fale if it is not, null if it is outside of the working directory.</returns> public bool? IsPathIgnored(string fullPath) { if (!PathUtils.IsAbsolute(fullPath)) { throw new ArgumentException(Resources.PathMustBeAbsolute, nameof(fullPath)); } // git uses the FS case-sensitivity for checking directory existence: var isDirectoryPath = PathUtils.HasTrailingDirectorySeparator(fullPath) || Directory.Exists(fullPath); var fullPathNoSlash = PathUtils.TrimTrailingSlash(PathUtils.ToPosixPath(Path.GetFullPath(fullPath))); if (isDirectoryPath && fullPathNoSlash.Equals(Ignore._workingDirectoryNoSlash, Ignore.PathComparison)) { return false; } return IsPathIgnored(fullPathNoSlash, isDirectoryPath); } private bool? IsPathIgnored(string normalizedPosixPath, bool isDirectoryPath) { Debug.Assert(PathUtils.IsAbsolute(normalizedPosixPath)); Debug.Assert(PathUtils.IsPosixPath(normalizedPosixPath)); Debug.Assert(!PathUtils.HasTrailingSlash(normalizedPosixPath)); // paths outside of working directory: if (!normalizedPosixPath.StartsWith(Ignore.WorkingDirectory, Ignore.PathComparison)) { return null; } if (isDirectoryPath && _directoryIgnoreStateCache.TryGetValue(normalizedPosixPath, out var isIgnored)) { return isIgnored; } isIgnored = IsIgnoredRecursive(normalizedPosixPath, isDirectoryPath); if (isDirectoryPath) { _directoryIgnoreStateCache.Add(normalizedPosixPath, isIgnored); } return isIgnored; } private bool IsIgnoredRecursive(string normalizedPosixPath, bool isDirectoryPath) { SplitPath(normalizedPosixPath, out var directory, out var fileName); if (directory == null || !directory.StartsWith(Ignore.WorkingDirectory, Ignore.PathComparison)) { return false; } var isIgnored = IsIgnored(normalizedPosixPath, directory, fileName, isDirectoryPath); if (isIgnored) { return true; } // The target file/directory itself is not ignored, but its containing directory might be. normalizedPosixPath = PathUtils.TrimTrailingSlash(directory); if (_directoryIgnoreStateCache.TryGetValue(normalizedPosixPath, out isIgnored)) { return isIgnored; } isIgnored = IsIgnoredRecursive(normalizedPosixPath, isDirectoryPath: true); _directoryIgnoreStateCache.Add(normalizedPosixPath, isIgnored); return isIgnored; } private static void SplitPath(string fullPath, out string? directoryWithSlash, out string fileName) { Debug.Assert(!PathUtils.HasTrailingSlash(fullPath)); var i = fullPath.LastIndexOf('/'); if (i < 0) { directoryWithSlash = null; fileName = fullPath; } else { directoryWithSlash = fullPath[..(i + 1)]; fileName = fullPath[(i + 1)..]; } } private bool IsIgnored(string normalizedPosixPath, string directory, string fileName, bool isDirectoryPath) { // Default patterns can't be overriden by a negative pattern: if (fileName.Equals(".git", Ignore.PathComparison)) { return true; } var isIgnored = false; // Visit groups in reverse order. // Patterns specified closer to the target file override those specified above. _reusableGroupList.Clear(); var groups = _reusableGroupList; for (PatternGroup? patternGroup = GetPatternGroup(directory); patternGroup != null; patternGroup = patternGroup.Parent) { groups.Add(patternGroup); } for (var i = groups.Count - 1; i >= 0; i--) { var patternGroup = groups[i]; if (!normalizedPosixPath.StartsWith(patternGroup.ContainingDirectory, Ignore.PathComparison)) { continue; } string? lazyRelativePath = null; foreach (var pattern in patternGroup.Patterns) { // If a pattern is matched as ignored only look for a negative pattern that matches as well. // If a pattern is not matched then skip negative patterns. if (isIgnored != pattern.IsNegative) { continue; } if (pattern.IsDirectoryPattern && !isDirectoryPath) { continue; } var matchPath = pattern.IsFullPathPattern ? lazyRelativePath ??= normalizedPosixPath[patternGroup.ContainingDirectory.Length..] : fileName; if (Glob.IsMatch(pattern.Glob, matchPath, Ignore.IgnoreCase, matchWildCardWithDirectorySeparator: false)) { // TODO: optimize negative pattern lookup (once we match, do we need to continue matching?) isIgnored = !pattern.IsNegative; } } } return isIgnored; } } } }