Skip to content

Commit

Permalink
Ignore and glob fix
Browse files Browse the repository at this point in the history
  • Loading branch information
tmat committed Jun 18, 2019
1 parent a4a2266 commit ff6678a
Show file tree
Hide file tree
Showing 7 changed files with 313 additions and 73 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ public Reader(string gitDirectory, string commonDirectory, GitEnvironment enviro
Debug.Assert(environment != null);

_environment = environment;
_gitDirectoryPosix = PathUtils.IsPosixDirectoryPath(gitDirectory);
_gitDirectoryPosix = PathUtils.ToPosixDirectoryPath(gitDirectory);
_commonDirectory = commonDirectory;
_fileOpener = fileOpener ?? File.OpenText;
}
Expand Down
200 changes: 155 additions & 45 deletions src/Microsoft.Build.Tasks.Git.Operations/Managed/GitIgnore.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

using System;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Diagnostics;
using System.IO;
using System.Text;
Expand All @@ -10,112 +11,218 @@ namespace Microsoft.Build.Tasks.Git
{
internal sealed class GitIgnore
{
internal readonly struct Pattern
internal sealed class PatternGroup
{
/// <summary>
/// Directory of the .gitignore file that defines the pattern.
/// Full posix slash terminated path.
/// </summary>
public string ContainingDirectory { get; }
public readonly string ContainingDirectory;

public readonly ImmutableArray<Pattern> Patterns;

public PatternFlags Flags { get; }
public string Glob { get; }
public readonly PatternGroup Parent;

public Pattern(string glob, string containingDirectory, PatternFlags flags)
public PatternGroup(PatternGroup parent, string containingDirectory, ImmutableArray<Pattern> patterns)
{
Debug.Assert(PathUtils.IsPosixPath(containingDirectory));
Debug.Assert(PathUtils.HasTrailingSlash(containingDirectory));

Glob = glob;
Parent = parent;
ContainingDirectory = containingDirectory;
Patterns = patterns;
}
}

internal readonly struct Pattern
{
public readonly PatternFlags Flags;
public readonly string Glob;

public Pattern(string glob, PatternFlags flags)
{
Debug.Assert(glob != null);

Glob = glob;
Flags = flags;
}

public bool IsDirectoryPattern => (Flags & PatternFlags.TrailingSlash) != 0;
public bool IsDirectoryPattern => (Flags & PatternFlags.DirectoryPattern) != 0;
public bool IsFullPathPattern => (Flags & PatternFlags.FullPath) != 0;
public bool IsNegative => (Flags & PatternFlags.Negative) != 0;
}

[Flags]
internal enum PatternFlags
{
None = 0,
Negate = 1,
TrailingSlash = 2,
LeadingSlash = 4,
FullPath = 8,
Negative = 1,
DirectoryPattern = 2,
FullPath = 4,
}

// defaults: ".", "..", ".git"
public static readonly GitIgnore Default = new GitIgnore();
private const string GitIgnoreFileName = ".gitignore";

/// <summary>
/// Full posix slash terminated path.
/// </summary>
private readonly string _workingDirectory;

private readonly bool _ignoreCase;

public GitIgnore()
/// <summary>
/// Maps full posix slash-terminated directory name to a pattern group.
/// </summary>
private readonly Dictionary<string, PatternGroup> _patternGroups;

private readonly PatternGroup _root;

internal GitIgnore(PatternGroup root, string workingDirectory, bool ignoreCase)
{
// var ignoreCase = GitConfig.ParseBooleanValue(config.GetVariableValue("core", "ignorecase"));
Debug.Assert(PathUtils.IsPosixPath(workingDirectory));
Debug.Assert(PathUtils.HasTrailingSlash(workingDirectory));

_ignoreCase = ignoreCase;
_workingDirectory = workingDirectory;
_root = root;
_patternGroups = new Dictionary<string, PatternGroup>();
}

public bool IsIgnored(string path, string workingDirectory, bool ignoreCase)
// TODO: OS path comparison?
private StringComparison PathComparison
=> _ignoreCase ? StringComparison.OrdinalIgnoreCase : StringComparison.Ordinal;

private bool IsDefaultPattern(string pattern)
=> pattern == "." || pattern == ".." || pattern.Equals(".git", PathComparison);

public bool? IsPathIgnored(string fullPath)
{
if (!PathUtils.IsAbsolute(path))
if (!PathUtils.IsAbsolute(fullPath))
{
path = Path.Combine(workingDirectory, path);
throw new ArgumentException("Path must be absolute", nameof(fullPath));
}

path = PathUtils.ToPosixPath(path);

bool isDirectoryPath = path[path.Length - 1] == '/' || Directory.Exists(path);
string directory = PathUtils.ToPosixDirectoryPath(Path.GetDirectoryName(fullPath));

path = PathUtils.TrimTrailingSlash(path);
// paths outside of working directory:
if (!directory.StartsWith(_workingDirectory, PathComparison))
{
return null;
}

var comparer = ignoreCase ? StringComparison.OrdinalIgnoreCase : StringComparison.Ordinal;
fullPath = PathUtils.ToPosixPath(fullPath);
bool isDirectoryPath = PathUtils.HasTrailingSlash(fullPath) || Directory.Exists(fullPath);
fullPath = PathUtils.TrimTrailingSlash(fullPath);

// Default patterns can't be overriden by a negative pattern:
string fileName = Path.GetFileName(fullPath);
if (IsDefaultPattern(fileName))
{
return true;
}

foreach (var pattern in EnumeratePatterns())
while (true)
{
if (!isDirectoryPath && pattern.IsDirectoryPattern)
bool isIgnored = false;

for (var patternGroup = GetPatternGroup(directory); patternGroup != null; patternGroup = patternGroup.Parent)
{
continue;
if (!fullPath.StartsWith(patternGroup.ContainingDirectory, PathComparison))
{
continue;
}

var relativePath = fullPath.Substring(patternGroup.ContainingDirectory.Length);

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 ? relativePath : fileName;
if (Glob.IsMatch(pattern.Glob, matchPath, _ignoreCase, matchWildCardWithDirectorySeparator: false))
{
// TODO: optimize negative pattern lookup (once we match, do we need to continue matching?)
isIgnored = !pattern.IsNegative;
}
}
}

if (!path.StartsWith(pattern.ContainingDirectory, comparer)) // TODO: OS path comparison?
if (isIgnored)
{
continue;
return true;
}


fullPath = PathUtils.ToPosixPath(Path.GetDirectoryName(fullPath));
fileName = Path.GetFileName(fullPath);
directory = PathUtils.ToPosixDirectoryPath(Path.GetDirectoryName(fullPath));
if (!directory.StartsWith(_workingDirectory, PathComparison))
{
return false;
}
}
}

private PatternGroup GetPatternGroup(string directory)
{
if (_patternGroups.TryGetValue(directory, out var group))
{
return group;
}

PatternGroup parent;
if (directory.Equals(_workingDirectory, PathComparison))
{
parent = _root;
}
else
{
parent = GetPatternGroup(PathUtils.ToPosixDirectoryPath(Path.GetDirectoryName(PathUtils.TrimTrailingSlash(directory))));
}

return false;
}
group = LoadFromFile(Path.Combine(directory, GitIgnoreFileName), parent) ?? parent;

private IEnumerable<Pattern> EnumeratePatterns()
{
throw new NotImplementedException();
_patternGroups.Add(directory, group);
return group;
}

/// <exception cref="IOException"/>
public static GitIgnore LoadFrom(string path)
/// <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

StreamReader reader;
if (!File.Exists(path))
{
return null;
}

StreamReader reader;
try
{
reader = File.OpenText(path);
}
catch (Exception e) when (e is FileNotFoundException || e is DirectoryNotFoundException)
{
return Default;
return null;
}

var reusableBuffer = new StringBuilder();

var directory = PathUtils.ToPosixDirectoryPath(Path.GetFullPath(Path.GetDirectoryName(path)));
var patterns = ImmutableArray.CreateBuilder<Pattern>();

using (reader)
{
var directory = PathUtils.IsPosixDirectoryPath(Path.GetDirectoryName(path));
var patterns = new List<Pattern>();

while (true)
{
string line = reader.ReadLine();
Expand All @@ -126,13 +233,17 @@ public static GitIgnore LoadFrom(string path)

if (TryParsePattern(line, reusableBuffer, out var glob, out var flags))
{
patterns.Add(new Pattern(glob, directory, flags));
patterns.Add(new Pattern(glob, flags));
}
}
}

if (patterns.Count == 0)
{
return null;
}

return new GitIgnore();
return new PatternGroup(parent, directory, patterns.ToImmutable());
}

internal static bool TryParsePattern(string line, StringBuilder reusableBuffer, out string glob, out PatternFlags flags)
Expand Down Expand Up @@ -174,7 +285,7 @@ internal static bool TryParsePattern(string line, StringBuilder reusableBuffer,
// Pattern negation.
if (line[s] == '!')
{
flags |= PatternFlags.Negate;
flags |= PatternFlags.Negative;
s++;
}

Expand All @@ -185,7 +296,7 @@ internal static bool TryParsePattern(string line, StringBuilder reusableBuffer,

if (line[e - 1] == '/')
{
flags |= PatternFlags.TrailingSlash;
flags |= PatternFlags.DirectoryPattern;
e--;
}

Expand All @@ -201,7 +312,6 @@ internal static bool TryParsePattern(string line, StringBuilder reusableBuffer,

if (line[s] == '/')
{
flags |= PatternFlags.LeadingSlash;
s++;
}

Expand Down
19 changes: 17 additions & 2 deletions src/Microsoft.Build.Tasks.Git.Operations/Managed/GitRepository.cs
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,8 @@ internal sealed class GitRepository

public GitConfig Config { get; }

public GitIgnore Ignore => _gitIgnore.Value;

/// <summary>
/// Full path.
/// </summary>
Expand All @@ -42,7 +44,8 @@ internal sealed class GitRepository

public GitEnvironment Environment { get; }

public readonly Lazy<ImmutableArray<GitSubmodule>> _submodules;
private readonly Lazy<ImmutableArray<GitSubmodule>> _submodules;
private readonly Lazy<GitIgnore> _gitIgnore;

internal GitRepository(GitEnvironment environment, GitConfig config, string gitDirectory, string commonDirectory, string workingDirectory)
{
Expand All @@ -58,6 +61,7 @@ internal GitRepository(GitEnvironment environment, GitConfig config, string gitD
Environment = environment;

_submodules = new Lazy<ImmutableArray<GitSubmodule>>(LoadSubmoduleConfiguration);
_gitIgnore = new Lazy<GitIgnore>(LoadIgnore);
}

/// <summary>
Expand Down Expand Up @@ -106,7 +110,7 @@ internal static string GetWorkingDirectory(GitConfig config, string gitDirectory

var gitdirFilePath = Path.Combine(gitDirectory, GitDirFileName);

var isLinkedWorkingTree = PathUtils.IsPosixDirectoryPath(commonDirectory) != PathUtils.IsPosixDirectoryPath(gitDirectory) &&
var isLinkedWorkingTree = PathUtils.ToPosixDirectoryPath(commonDirectory) != PathUtils.ToPosixDirectoryPath(gitDirectory) &&
File.Exists(gitdirFilePath);

if (isLinkedWorkingTree)
Expand Down Expand Up @@ -331,6 +335,17 @@ private ImmutableArray<GitSubmodule> LoadSubmoduleConfiguration()
return builder.ToImmutable();
}

private GitIgnore LoadIgnore()
{
var workingDirectory = GetWorkingDirectory();
var ignoreCase = GitConfig.ParseBooleanValue(Config.GetVariableValue("core", "ignorecase"));
var excludesFile = Config.GetVariableValue("core", "excludesFile");
var commonInfoExclude = Path.Combine(CommonDirectory, "info", "exclude");

var root = GitIgnore.LoadFromFile(commonInfoExclude, GitIgnore.LoadFromFile(excludesFile, parent: null));
return new GitIgnore(root, workingDirectory, ignoreCase);
}

/// <exception cref="IOException" />
/// <exception cref="InvalidDataException" />
internal static bool LocateRepository(string directory, out string gitDirectory, out string commonDirectory, out string workingDirectory)
Expand Down

0 comments on commit ff6678a

Please sign in to comment.