Skip to content

Commit

Permalink
add case-sensitive matching/sorting on repo paths
Browse files Browse the repository at this point in the history
Introduce case-sensitive file path matching and sorting
of file and folder names within the repository as well as
the modified paths and placeholder list databases, but only
on Linux and other case-sensitive file systems, while
retaining existing case-insensitive behaviour on Windows
and Mac platforms.

Also make sure to exclude the case-sensitive filesystem
unit tests when running on Windows or Mac platforms.
  • Loading branch information
chrisd8088 committed Aug 23, 2019
1 parent e6eb054 commit abd8a62
Show file tree
Hide file tree
Showing 27 changed files with 215 additions and 73 deletions.
4 changes: 2 additions & 2 deletions GVFS/GVFS.Common/Database/GVFSDatabase.cs
Original file line number Diff line number Diff line change
Expand Up @@ -117,8 +117,8 @@ private void Initialize()
command.ExecuteNonQuery();
}

PlaceholderTable.CreateTable(connection);
SparseTable.CreateTable(connection);
PlaceholderTable.CreateTable(connection, GVFSPlatform.Instance.Constants.CaseSensitiveFileSystem);
SparseTable.CreateTable(connection, GVFSPlatform.Instance.Constants.CaseSensitiveFileSystem);
}
}

Expand Down
7 changes: 4 additions & 3 deletions GVFS/GVFS.Common/Database/PlaceholderTable.cs
Original file line number Diff line number Diff line change
Expand Up @@ -17,11 +17,12 @@ public PlaceholderTable(IGVFSConnectionPool connectionPool)
this.connectionPool = connectionPool;
}

public static void CreateTable(IDbConnection connection)
public static void CreateTable(IDbConnection connection, bool caseSensitiveFileSystem)
{
using (IDbCommand command = connection.CreateCommand())
{
command.CommandText = "CREATE TABLE IF NOT EXISTS [Placeholder] (path TEXT PRIMARY KEY COLLATE NOCASE, pathType TINYINT NOT NULL, sha char(40) ) WITHOUT ROWID;";
string collateConstraint = caseSensitiveFileSystem ? string.Empty : " COLLATE NOCASE";
command.CommandText = $"CREATE TABLE IF NOT EXISTS [Placeholder] (path TEXT PRIMARY KEY{collateConstraint}, pathType TINYINT NOT NULL, sha char(40) ) WITHOUT ROWID;";
command.ExecuteNonQuery();
}
}
Expand Down Expand Up @@ -267,4 +268,4 @@ public enum PlaceholderType
public bool IsPossibleTombstoneFolder => this.PathType == PlaceholderType.PossibleTombstoneFolder;
}
}
}
}
7 changes: 4 additions & 3 deletions GVFS/GVFS.Common/Database/SparseTable.cs
Original file line number Diff line number Diff line change
Expand Up @@ -20,11 +20,12 @@ public static string NormalizePath(string path)
return path.Replace(Path.AltDirectorySeparatorChar, Path.DirectorySeparatorChar).Trim().Trim(Path.DirectorySeparatorChar);
}

public static void CreateTable(IDbConnection connection)
public static void CreateTable(IDbConnection connection, bool caseSensitiveFileSystem)
{
using (IDbCommand command = connection.CreateCommand())
{
command.CommandText = "CREATE TABLE IF NOT EXISTS [Sparse] (path TEXT PRIMARY KEY COLLATE NOCASE) WITHOUT ROWID;";
string collateConstraint = caseSensitiveFileSystem ? string.Empty : " COLLATE NOCASE";
command.CommandText = $"CREATE TABLE IF NOT EXISTS [Sparse] (path TEXT PRIMARY KEY{collateConstraint}) WITHOUT ROWID;";
command.ExecuteNonQuery();
}
}
Expand Down Expand Up @@ -58,7 +59,7 @@ public HashSet<string> GetAll()
using (IDbConnection connection = this.connectionPool.GetConnection())
using (IDbCommand command = connection.CreateCommand())
{
HashSet<string> directories = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
HashSet<string> directories = new HashSet<string>(GVFSPlatform.Instance.Constants.PathComparer);
command.CommandText = $"SELECT path FROM Sparse;";
using (IDataReader reader = command.ExecuteReader())
{
Expand Down
10 changes: 5 additions & 5 deletions GVFS/GVFS.Common/HealthCalculator/EnlistmentHealthCalculator.cs
Original file line number Diff line number Diff line change
Expand Up @@ -26,8 +26,8 @@ public EnlistmentHealthData CalculateStatistics(string parentDirectory)
int gitTrackedItemsCount = 0;
int placeholderCount = 0;
int modifiedPathsCount = 0;
Dictionary<string, int> gitTrackedItemsDirectoryTally = new Dictionary<string, int>(StringComparer.OrdinalIgnoreCase);
Dictionary<string, int> hydratedFilesDirectoryTally = new Dictionary<string, int>(StringComparer.OrdinalIgnoreCase);
Dictionary<string, int> gitTrackedItemsDirectoryTally = new Dictionary<string, int>(GVFSPlatform.Instance.Constants.PathComparer);
Dictionary<string, int> hydratedFilesDirectoryTally = new Dictionary<string, int>(GVFSPlatform.Instance.Constants.PathComparer);

// Parent directory is a path relative to the root of the repository which is already in git format
if (!parentDirectory.EndsWith(GVFSConstants.GitPathSeparatorString) && parentDirectory.Length > 0)
Expand All @@ -47,7 +47,7 @@ public EnlistmentHealthData CalculateStatistics(string parentDirectory)
modifiedPathsCount += this.CategorizePaths(this.enlistmentPathData.ModifiedFolderPaths, hydratedFilesDirectoryTally, parentDirectory);
modifiedPathsCount += this.CategorizePaths(this.enlistmentPathData.ModifiedFilePaths, hydratedFilesDirectoryTally, parentDirectory);

Dictionary<string, SubDirectoryInfo> mostHydratedDirectories = new Dictionary<string, SubDirectoryInfo>(StringComparer.OrdinalIgnoreCase);
Dictionary<string, SubDirectoryInfo> mostHydratedDirectories = new Dictionary<string, SubDirectoryInfo>(GVFSPlatform.Instance.Constants.PathComparer);

// Map directory names to the corresponding health data from gitTrackedItemsDirectoryTally and hydratedFilesDirectoryTally
foreach (KeyValuePair<string, int> pair in gitTrackedItemsDirectoryTally)
Expand Down Expand Up @@ -107,12 +107,12 @@ private int CategorizePaths(IEnumerable<string> paths, Dictionary<string, int> d
foreach (string path in paths)
{
// Only categorize if descendent of the parentDirectory
if (path.StartsWith(parentDirectory, StringComparison.OrdinalIgnoreCase))
if (path.StartsWith(parentDirectory, GVFSPlatform.Instance.Constants.PathComparison))
{
count++;

// If the path is to the parentDirectory, ignore it to avoid adding string.Empty to the data structures
if (!parentDirectory.Equals(path, StringComparison.OrdinalIgnoreCase))
if (!parentDirectory.Equals(path, GVFSPlatform.Instance.Constants.PathComparison))
{
// Trim the path to parent directory
string topDir = this.ParseTopDirectory(this.TrimDirectoryFromPath(path, parentDirectory));
Expand Down
2 changes: 1 addition & 1 deletion GVFS/GVFS.Common/LegacyPlaceholderListDatabase.cs
Original file line number Diff line number Diff line change
Expand Up @@ -275,7 +275,7 @@ public void WriteAllEntriesAndFlush(IEnumerable<IPlaceholderData> updatedPlaceho

private IEnumerable<string> GenerateDataLines(IEnumerable<IPlaceholderData> updatedPlaceholders)
{
HashSet<string> keys = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
HashSet<string> keys = new HashSet<string>(GVFSPlatform.Instance.Constants.PathComparer);

this.count = 0;
foreach (IPlaceholderData updated in updatedPlaceholders)
Expand Down
2 changes: 1 addition & 1 deletion GVFS/GVFS.Common/ModifiedPathsDatabase.cs
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ public class ModifiedPathsDatabase : FileBasedCollection
protected ModifiedPathsDatabase(ITracer tracer, PhysicalFileSystem fileSystem, string dataFilePath)
: base(tracer, fileSystem, dataFilePath, collectionAppendsDirectlyToFile: true)
{
this.modifiedPaths = new ConcurrentHashSet<string>(StringComparer.OrdinalIgnoreCase);
this.modifiedPaths = new ConcurrentHashSet<string>(GVFSPlatform.Instance.Constants.PathComparer);
}

public int Count
Expand Down
2 changes: 1 addition & 1 deletion GVFS/GVFS.Platform.Mac/MacFileSystemVirtualizer.cs
Original file line number Diff line number Diff line change
Expand Up @@ -495,7 +495,7 @@ private Result OnPreDelete(string relativePath, bool isDirectory)
bool pathInsideDotGit = Virtualization.FileSystemCallbacks.IsPathInsideDotGit(relativePath);
if (pathInsideDotGit)
{
if (relativePath.Equals(GVFSConstants.DotGit.Index, StringComparison.OrdinalIgnoreCase))
if (relativePath.Equals(GVFSConstants.DotGit.Index, GVFSPlatform.Instance.Constants.PathComparison))
{
string lockedGitCommand = this.Context.Repository.GVFSLock.GetLockedGitCommand();
if (string.IsNullOrEmpty(lockedGitCommand))
Expand Down
5 changes: 3 additions & 2 deletions GVFS/GVFS.Platform.Windows/ActiveEnumeration.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
using GVFS.Virtualization.Projection;
using GVFS.Common;
using GVFS.Virtualization.Projection;
using Microsoft.Windows.ProjFS;
using System.Collections.Generic;

Expand Down Expand Up @@ -111,7 +112,7 @@ public string GetFilterString()

private static bool NameMatchesNoWildcardFilter(string name, string filter)
{
return string.Equals(name, filter, System.StringComparison.OrdinalIgnoreCase);
return string.Equals(name, filter, GVFSPlatform.Instance.Constants.PathComparison);
}

private void SaveFilter(string filter)
Expand Down
5 changes: 3 additions & 2 deletions GVFS/GVFS.Platform.Windows/PatternMatcher.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
using System;
using GVFS.Common;
using System;

namespace GVFS.Platform.Windows
{
Expand Down Expand Up @@ -173,7 +174,7 @@ public static bool StrictMatchPattern(string expression, string name)
// if name is shorter that the stuff to the right of * in expression, we don't
// need to do the string compare, otherwise we compare rightlength characters
// and the end of both strings.
if (name.Length >= rightLength && string.Compare(expression, 1, name, name.Length - rightLength, rightLength, StringComparison.OrdinalIgnoreCase) == 0)
if (name.Length >= rightLength && string.Compare(expression, 1, name, name.Length - rightLength, rightLength, GVFSPlatform.Instance.Constants.PathComparison) == 0)
{
return true;
}
Expand Down
2 changes: 1 addition & 1 deletion GVFS/GVFS.Platform.Windows/WindowsFileSystemVirtualizer.cs
Original file line number Diff line number Diff line change
Expand Up @@ -1090,7 +1090,7 @@ private bool NotifyPreRenameHandler(string relativePath, string destinationPath,
{
try
{
if (destinationPath.Equals(GVFSConstants.DotGit.Index, StringComparison.OrdinalIgnoreCase))
if (destinationPath.Equals(GVFSConstants.DotGit.Index, GVFSPlatform.Instance.Constants.PathComparison))
{
string lockedGitCommand = this.Context.Repository.GVFSLock.GetLockedGitCommand();
if (string.IsNullOrEmpty(lockedGitCommand))
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
using GVFS.Common.Git;
using GVFS.Common;
using GVFS.Common.Git;
using GVFS.Platform.Windows;
using GVFS.Tests.Should;
using GVFS.Virtualization.Projection;
Expand Down Expand Up @@ -228,49 +229,49 @@ public void EnumerateMultipleEntryListWithWildcardFilter()

activeEnumeration = CreateActiveEnumeration(entries);
activeEnumeration.TrySaveFilterString("*.txt").ShouldEqual(true);
this.ValidateActiveEnumeratorReturnsAllEntries(activeEnumeration, entries.Where(entry => entry.Name.EndsWith(".txt", System.StringComparison.OrdinalIgnoreCase)));
this.ValidateActiveEnumeratorReturnsAllEntries(activeEnumeration, entries.Where(entry => entry.Name.EndsWith(".txt", GVFSPlatform.Instance.Constants.PathComparison)));

// '<' = DOS_STAR, matches 0 or more characters until encountering and matching
// the final . in the name
activeEnumeration = CreateActiveEnumeration(entries);
activeEnumeration.TrySaveFilterString("<.txt").ShouldEqual(true);
this.ValidateActiveEnumeratorReturnsAllEntries(activeEnumeration, entries.Where(entry => entry.Name.EndsWith(".txt", System.StringComparison.OrdinalIgnoreCase)));
this.ValidateActiveEnumeratorReturnsAllEntries(activeEnumeration, entries.Where(entry => entry.Name.EndsWith(".txt", GVFSPlatform.Instance.Constants.PathComparison)));

activeEnumeration = CreateActiveEnumeration(entries);
activeEnumeration.TrySaveFilterString("?").ShouldEqual(true);
this.ValidateActiveEnumeratorReturnsAllEntries(activeEnumeration, entries.Where(entry => entry.Name.Length == 1));

activeEnumeration = CreateActiveEnumeration(entries);
activeEnumeration.TrySaveFilterString("?.txt").ShouldEqual(true);
this.ValidateActiveEnumeratorReturnsAllEntries(activeEnumeration, entries.Where(entry => entry.Name.Length == 5 && entry.Name.EndsWith(".txt", System.StringComparison.OrdinalIgnoreCase)));
this.ValidateActiveEnumeratorReturnsAllEntries(activeEnumeration, entries.Where(entry => entry.Name.Length == 5 && entry.Name.EndsWith(".txt", GVFSPlatform.Instance.Constants.PathComparison)));

// '>' = DOS_QM, matches any single character, or upon encountering a period or
// end of name string, advances the expression to the end of the
// set of contiguous DOS_QMs.
activeEnumeration = CreateActiveEnumeration(entries);
activeEnumeration.TrySaveFilterString(">.txt").ShouldEqual(true);
this.ValidateActiveEnumeratorReturnsAllEntries(activeEnumeration, entries.Where(entry => entry.Name.Length <= 5 && entry.Name.EndsWith(".txt", System.StringComparison.OrdinalIgnoreCase)));
this.ValidateActiveEnumeratorReturnsAllEntries(activeEnumeration, entries.Where(entry => entry.Name.Length <= 5 && entry.Name.EndsWith(".txt", GVFSPlatform.Instance.Constants.PathComparison)));

activeEnumeration = CreateActiveEnumeration(entries);
activeEnumeration.TrySaveFilterString("E.???").ShouldEqual(true);
this.ValidateActiveEnumeratorReturnsAllEntries(activeEnumeration, entries.Where(entry => entry.Name.Length == 5 && entry.Name.StartsWith("E.", System.StringComparison.OrdinalIgnoreCase)));
this.ValidateActiveEnumeratorReturnsAllEntries(activeEnumeration, entries.Where(entry => entry.Name.Length == 5 && entry.Name.StartsWith("E.", GVFSPlatform.Instance.Constants.PathComparison)));

// '"' = DOS_DOT, matches either a . or zero characters beyond name string.
activeEnumeration = CreateActiveEnumeration(entries);
activeEnumeration.TrySaveFilterString("E\"*").ShouldEqual(true);
this.ValidateActiveEnumeratorReturnsAllEntries(activeEnumeration, entries.Where(entry => entry.Name.StartsWith("E.", System.StringComparison.OrdinalIgnoreCase) || entry.Name.Equals("E", System.StringComparison.OrdinalIgnoreCase)));
this.ValidateActiveEnumeratorReturnsAllEntries(activeEnumeration, entries.Where(entry => entry.Name.StartsWith("E.", GVFSPlatform.Instance.Constants.PathComparison) || entry.Name.Equals("E", GVFSPlatform.Instance.Constants.PathComparison)));

activeEnumeration = CreateActiveEnumeration(entries);
activeEnumeration.TrySaveFilterString("e\"*").ShouldEqual(true);
this.ValidateActiveEnumeratorReturnsAllEntries(activeEnumeration, entries.Where(entry => entry.Name.StartsWith("E.", System.StringComparison.OrdinalIgnoreCase) || entry.Name.Equals("E", System.StringComparison.OrdinalIgnoreCase)));
this.ValidateActiveEnumeratorReturnsAllEntries(activeEnumeration, entries.Where(entry => entry.Name.StartsWith("E.", GVFSPlatform.Instance.Constants.PathComparison) || entry.Name.Equals("E", GVFSPlatform.Instance.Constants.PathComparison)));

activeEnumeration = CreateActiveEnumeration(entries);
activeEnumeration.TrySaveFilterString("B\"*").ShouldEqual(true);
this.ValidateActiveEnumeratorReturnsAllEntries(activeEnumeration, entries.Where(entry => entry.Name.StartsWith("B.", System.StringComparison.OrdinalIgnoreCase) || entry.Name.Equals("B", System.StringComparison.OrdinalIgnoreCase)));
this.ValidateActiveEnumeratorReturnsAllEntries(activeEnumeration, entries.Where(entry => entry.Name.StartsWith("B.", GVFSPlatform.Instance.Constants.PathComparison) || entry.Name.Equals("B", GVFSPlatform.Instance.Constants.PathComparison)));

activeEnumeration = CreateActiveEnumeration(entries);
activeEnumeration.TrySaveFilterString("e.???").ShouldEqual(true);
this.ValidateActiveEnumeratorReturnsAllEntries(activeEnumeration, entries.Where(entry => entry.Name.Length == 5 && entry.Name.StartsWith("E.", System.StringComparison.OrdinalIgnoreCase)));
this.ValidateActiveEnumeratorReturnsAllEntries(activeEnumeration, entries.Where(entry => entry.Name.Length == 5 && entry.Name.StartsWith("E.", GVFSPlatform.Instance.Constants.PathComparison)));
}

[TestCase]
Expand Down Expand Up @@ -429,4 +430,4 @@ public PatternMatcherWrapper(ActiveEnumeration.FileNamePatternMatcher matcher)
public ActiveEnumeration.FileNamePatternMatcher Matcher { get; }
}
}
}
}
1 change: 1 addition & 0 deletions GVFS/GVFS.UnitTests/Category/CategoryConstants.cs
Original file line number Diff line number Diff line change
Expand Up @@ -4,5 +4,6 @@ public static class CategoryConstants
{
public const string ExceptionExpected = "ExceptionExpected";
public const string CaseInsensitiveFileSystemOnly = "CaseInsensitiveFileSystemOnly";
public const string CaseSensitiveFileSystemOnly = "CaseSensitiveFileSystemOnly";
}
}
8 changes: 5 additions & 3 deletions GVFS/GVFS.UnitTests/Common/Database/GVFSDatabaseTests.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
using GVFS.Common.Database;
using GVFS.Common;
using GVFS.Common.Database;
using GVFS.Tests.Should;
using GVFS.UnitTests.Category;
using GVFS.UnitTests.Mock.FileSystem;
Expand Down Expand Up @@ -74,8 +75,9 @@ private void TestGVFSDatabase(Action<GVFSDatabase> testCode, bool throwException
mockCommand.Setup(x => x.ExecuteScalar()).Returns(1);
mockCommand.Setup(x => x.Dispose());

string collateConstraint = GVFSPlatform.Instance.Constants.CaseSensitiveFileSystem ? string.Empty : " COLLATE NOCASE";
Mock<IDbCommand> mockCommand2 = new Mock<IDbCommand>(MockBehavior.Strict);
mockCommand2.SetupSet(x => x.CommandText = "CREATE TABLE IF NOT EXISTS [Placeholder] (path TEXT PRIMARY KEY COLLATE NOCASE, pathType TINYINT NOT NULL, sha char(40) ) WITHOUT ROWID;");
mockCommand2.SetupSet(x => x.CommandText = $"CREATE TABLE IF NOT EXISTS [Placeholder] (path TEXT PRIMARY KEY{collateConstraint}, pathType TINYINT NOT NULL, sha char(40) ) WITHOUT ROWID;");
if (throwException)
{
mockCommand2.Setup(x => x.ExecuteNonQuery()).Throws(new Exception("Error"));
Expand All @@ -88,7 +90,7 @@ private void TestGVFSDatabase(Action<GVFSDatabase> testCode, bool throwException
mockCommand2.Setup(x => x.Dispose());

Mock<IDbCommand> mockCommand3 = new Mock<IDbCommand>(MockBehavior.Strict);
mockCommand3.SetupSet(x => x.CommandText = "CREATE TABLE IF NOT EXISTS [Sparse] (path TEXT PRIMARY KEY COLLATE NOCASE) WITHOUT ROWID;");
mockCommand3.SetupSet(x => x.CommandText = $"CREATE TABLE IF NOT EXISTS [Sparse] (path TEXT PRIMARY KEY{collateConstraint}) WITHOUT ROWID;");
if (throwException)
{
mockCommand3.Setup(x => x.ExecuteNonQuery()).Throws(new Exception("Error"));
Expand Down
4 changes: 2 additions & 2 deletions GVFS/GVFS.UnitTests/Common/Database/PlaceholderTableTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -495,9 +495,9 @@ protected override PlaceholderTable TableFactory(IGVFSConnectionPool pool)
return new PlaceholderTable(pool);
}

protected override void CreateTable(IDbConnection connection)
protected override void CreateTable(IDbConnection connection, bool caseSensitiveFileSystem)
{
PlaceholderTable.CreateTable(connection);
PlaceholderTable.CreateTable(connection, caseSensitiveFileSystem);
}

private void TestPlaceholdersInsert(Action<PlaceholderTable> testCode, string path, int pathType, string sha, bool throwException = false)
Expand Down
4 changes: 2 additions & 2 deletions GVFS/GVFS.UnitTests/Common/Database/SparseTableTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -127,9 +127,9 @@ protected override SparseTable TableFactory(IGVFSConnectionPool pool)
return new SparseTable(pool);
}

protected override void CreateTable(IDbConnection connection)
protected override void CreateTable(IDbConnection connection, bool caseSensitiveFileSystem)
{
SparseTable.CreateTable(connection);
SparseTable.CreateTable(connection, caseSensitiveFileSystem);
}

private static string CombineAltForTrim(char character, params string[] folders)
Expand Down
Loading

0 comments on commit abd8a62

Please sign in to comment.