Skip to content

Commit f9e5019

Browse files
committed
GitIgnore optimizations
1 parent 111d9af commit f9e5019

File tree

3 files changed

+302
-210
lines changed

3 files changed

+302
-210
lines changed
Lines changed: 235 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,235 @@
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

Comments
 (0)