| File: GitDataReader\GitIgnore.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.Collections.Immutable; using System.Diagnostics; using System.Diagnostics.CodeAnalysis; using System.IO; using System.Text; namespace Microsoft.Build.Tasks.Git { internal sealed partial class GitIgnore { internal sealed class PatternGroup { /// <summary> /// Directory of the .gitignore file that defines the pattern. /// Full posix slash terminated path. /// </summary> public readonly string ContainingDirectory; public readonly ImmutableArray<Pattern> Patterns; public readonly PatternGroup? Parent; public PatternGroup(PatternGroup? parent, string containingDirectory, ImmutableArray<Pattern> patterns) { NullableDebug.Assert(PathUtils.IsPosixPath(containingDirectory)); NullableDebug.Assert(PathUtils.HasTrailingSlash(containingDirectory)); Parent = parent; ContainingDirectory = containingDirectory; Patterns = patterns; } } internal readonly struct Pattern { public readonly PatternFlags Flags; public readonly string Glob; public Pattern(string glob, PatternFlags flags) { Glob = glob; Flags = flags; } public bool IsDirectoryPattern => (Flags & PatternFlags.DirectoryPattern) != 0; public bool IsFullPathPattern => (Flags & PatternFlags.FullPath) != 0; public bool IsNegative => (Flags & PatternFlags.Negative) != 0; public override string ToString() => $"{(IsNegative ? "!" : "")}{Glob}{(IsDirectoryPattern ? " <dir>" : "")}{(IsFullPathPattern ? " <path>" : "")}"; } [Flags] internal enum PatternFlags { None = 0, Negative = 1, DirectoryPattern = 2, FullPath = 4, } private const string GitIgnoreFileName = ".gitignore"; /// <summary> /// Full posix slash terminated path. /// </summary> public string WorkingDirectory { get; } private readonly string _workingDirectoryNoSlash; public bool IgnoreCase { get; } public PatternGroup? Root { get; } internal GitIgnore(PatternGroup? root, string workingDirectory, bool ignoreCase) { NullableDebug.Assert(PathUtils.IsAbsolute(workingDirectory)); IgnoreCase = ignoreCase; WorkingDirectory = PathUtils.ToPosixDirectoryPath(workingDirectory); _workingDirectoryNoSlash = PathUtils.TrimTrailingSlash(WorkingDirectory); Root = root; } private StringComparison PathComparison => IgnoreCase ? StringComparison.OrdinalIgnoreCase : StringComparison.Ordinal; private IEqualityComparer<string> PathComparer => IgnoreCase ? StringComparer.OrdinalIgnoreCase : StringComparer.Ordinal; public Matcher CreateMatcher() => new Matcher(this); /// <exception cref="IOException"/> /// <exception cref="ArgumentException"><paramref name="path"/> is invalid</exception> internal static PatternGroup? LoadFromFile(string? path, PatternGroup? parent) { // See https://git-scm.com/docs/gitignore#_pattern_format if (!File.Exists(path)) { return null; } StreamReader reader; try { reader = File.OpenText(path!); } catch (Exception e) when (e is FileNotFoundException or DirectoryNotFoundException) { return null; } var reusableBuffer = new StringBuilder(); var directory = PathUtils.ToPosixDirectoryPath(Path.GetFullPath(Path.GetDirectoryName(path)!)); var patterns = ImmutableArray.CreateBuilder<Pattern>(); using (reader) { while (true) { var line = reader.ReadLine(); if (line == null) { break; } if (TryParsePattern(line, reusableBuffer, out var glob, out var flags)) { patterns.Add(new Pattern(glob, flags)); } } } if (patterns.Count == 0) { return null; } return new PatternGroup(parent, directory, patterns.ToImmutable()); } internal static bool TryParsePattern(string line, StringBuilder reusableBuffer, [NotNullWhen(true)]out string? glob, out PatternFlags flags) { glob = null; flags = PatternFlags.None; // Trailing spaces are ignored unless '\'-escaped. // Leading spaces are significant. // Other whitespace (\t, \v, \f) is significant. var e = line.Length - 1; while (e >= 0 && line[e] == ' ') { e--; } e++; // Skip blank line. if (e == 0) { return false; } // put trailing space back if escaped: if (e < line.Length && line[e] == ' ' && line[e - 1] == '\\') { e++; } var s = 0; // Skip comment. if (line[s] == '#') { return false; } // Pattern negation. if (line[s] == '!') { flags |= PatternFlags.Negative; s++; } if (s == e) { return false; } if (line[e - 1] == '/') { flags |= PatternFlags.DirectoryPattern; e--; } if (s == e) { return false; } if (line.IndexOf('/', s, e - s) >= 0) { flags |= PatternFlags.FullPath; } if (line[s] == '/') { s++; } if (s == e) { return false; } var escape = line.IndexOf('\\', s, e - s); if (escape < 0) { glob = line[s..e]; return true; } reusableBuffer.Clear(); reusableBuffer.Append(line, s, escape - s); var i = escape; while (i < e) { var c = line[i++]; if (c == '\\' && i < e) { c = line[i++]; } reusableBuffer.Append(c); } glob = reusableBuffer.ToString(); return true; } } }