diff --git a/GVFS/FastFetch/FastFetch.csproj b/GVFS/FastFetch/FastFetch.csproj
index d79981ec13..5658cdadea 100644
--- a/GVFS/FastFetch/FastFetch.csproj
+++ b/GVFS/FastFetch/FastFetch.csproj
@@ -64,7 +64,9 @@
-
+
+ Designer
+
@@ -86,11 +88,11 @@
This project references NuGet package(s) that are missing on this computer. Use NuGet Package Restore to download them. For more information, see http://go.microsoft.com/fwlink/?LinkID=322105. The missing file is {0}.
-
+
-
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/GVFS/GVFS.Common/GVFSConstants.cs b/GVFS/GVFS.Common/GVFSConstants.cs
index e83fe51406..f498773f1f 100644
--- a/GVFS/GVFS.Common/GVFSConstants.cs
+++ b/GVFS/GVFS.Common/GVFSConstants.cs
@@ -25,6 +25,7 @@ public static class GitConfig
public const string GVFSPrefix = "gvfs.";
public const string MaxRetriesConfig = GVFSPrefix + "max-retries";
public const string TimeoutSecondsConfig = GVFSPrefix + "timeout-seconds";
+ public const string GitStatusCacheBackoffConfig = GVFSPrefix + "status-cache-backoff-seconds";
public const string MountId = GVFSPrefix + "mount-id";
public const string EnlistmentId = GVFSPrefix + "enlistment-id";
public const string CacheServer = GVFSPrefix + "cache-server";
@@ -34,6 +35,11 @@ public static class GitConfig
public const string HooksExtension = ".hooks";
}
+ public static class GitStatusCache
+ {
+ public const string EnableGitStatusCacheTokenFile = "EnableGitStatusCacheToken.dat";
+ }
+
public static class Service
{
public const string ServiceName = "GVFS.Service";
@@ -94,6 +100,12 @@ public static class Databases
public static readonly string ModifiedPaths = Path.Combine(Name, "ModifiedPaths.dat");
public static readonly string RepoMetadata = Path.Combine(Name, "RepoMetadata.dat");
}
+
+ public static class GitStatusCache
+ {
+ public const string Name = "gitStatusCache";
+ public static readonly string CachePath = Path.Combine(Name, "GitStatusCache.dat");
+ }
}
public static class DotGit
@@ -175,6 +187,7 @@ public static class Refs
public static class Heads
{
public static readonly string Root = Path.Combine(DotGit.Refs.Root, "heads");
+ public static readonly string RootFolder = Heads.Root + Path.DirectorySeparatorChar;
}
}
}
diff --git a/GVFS/GVFS.Common/GVFSEnlistment.cs b/GVFS/GVFS.Common/GVFSEnlistment.cs
index 297466f59f..345bfd8d46 100644
--- a/GVFS/GVFS.Common/GVFSEnlistment.cs
+++ b/GVFS/GVFS.Common/GVFSEnlistment.cs
@@ -31,6 +31,8 @@ public GVFSEnlistment(string enlistmentRoot, string repoUrl, string gitBinPath,
{
this.NamedPipeName = GVFSPlatform.Instance.GetNamedPipeName(this.EnlistmentRoot);
this.DotGVFSRoot = Path.Combine(this.EnlistmentRoot, GVFSConstants.DotGVFS.Root);
+ this.GitStatusCacheFolder = Path.Combine(this.DotGVFSRoot, GVFSConstants.DotGVFS.GitStatusCache.Name);
+ this.GitStatusCachePath = Path.Combine(this.DotGVFSRoot, GVFSConstants.DotGVFS.GitStatusCache.CachePath);
this.GVFSLogsRoot = Path.Combine(this.EnlistmentRoot, GVFSConstants.DotGVFS.LogPath);
this.LocalObjectsRoot = Path.Combine(this.WorkingDirectoryRoot, GVFSConstants.DotGit.Objects.Root);
}
@@ -58,6 +60,8 @@ private GVFSEnlistment(string enlistmentRoot, string gitBinPath, string gvfsHook
public override string GitObjectsRoot { get; protected set; }
public override string LocalObjectsRoot { get; protected set; }
public override string GitPackRoot { get; protected set; }
+ public string GitStatusCacheFolder { get; private set; }
+ public string GitStatusCachePath { get; private set; }
// These version properties are only used in logging during clone and mount to track version numbers
public string GitVersion
@@ -74,7 +78,7 @@ public string GVFSHooksVersion
{
get { return this.gvfsHooksVersion; }
}
-
+
public static GVFSEnlistment CreateWithoutRepoUrlFromDirectory(string directory, string gitBinRoot, string gvfsHooksRoot)
{
if (Directory.Exists(directory))
diff --git a/GVFS/GVFS.Common/GVFSPlatform.cs b/GVFS/GVFS.Common/GVFSPlatform.cs
index 6ac2f3110a..cb0b7f2d4d 100644
--- a/GVFS/GVFS.Common/GVFSPlatform.cs
+++ b/GVFS/GVFS.Common/GVFSPlatform.cs
@@ -57,6 +57,8 @@ public static void Register(GVFSPlatform platform)
public abstract bool IsConsoleOutputRedirectedToFile();
public abstract bool TryGetGVFSEnlistmentRoot(string directory, out string enlistmentRoot, out string errorMessage);
+ public abstract bool IsGitStatusCacheSupported();
+
public bool TryGetNormalizedPathRoot(string path, out string pathRoot, out string errorMessage)
{
pathRoot = null;
diff --git a/GVFS/GVFS.Common/Git/GitObjects.cs b/GVFS/GVFS.Common/Git/GitObjects.cs
index 7b44598845..22e6eab099 100644
--- a/GVFS/GVFS.Common/Git/GitObjects.cs
+++ b/GVFS/GVFS.Common/Git/GitObjects.cs
@@ -450,6 +450,11 @@ public virtual string[] ReadPackFileNames(string packFolderPath, string prefixFi
return new string[0];
}
+ public virtual bool IsUsingCacheServer()
+ {
+ return !this.GitObjectRequestor.CacheServer.IsNone(this.Enlistment.RepoUrl);
+ }
+
private static string GetRandomPackName(string packRoot)
{
string packName = "pack-" + Guid.NewGuid().ToString("N") + ".pack";
diff --git a/GVFS/GVFS.Common/Git/GitProcess.cs b/GVFS/GVFS.Common/Git/GitProcess.cs
index 69f9c48c51..47ad144e83 100644
--- a/GVFS/GVFS.Common/Git/GitProcess.cs
+++ b/GVFS/GVFS.Common/Git/GitProcess.cs
@@ -271,9 +271,20 @@ public Result ForceCheckout(string target)
return this.InvokeGitInWorkingDirectoryRoot("checkout -f " + target, useReadObjectHook: false);
}
- public Result Status(bool allowObjectDownloads)
+ public Result Status(bool allowObjectDownloads, bool useStatusCache)
{
- return this.InvokeGitInWorkingDirectoryRoot("status", useReadObjectHook: allowObjectDownloads);
+ string command = useStatusCache ? "status" : "status --no-deserialize";
+ return this.InvokeGitInWorkingDirectoryRoot(command, useReadObjectHook: allowObjectDownloads);
+ }
+
+ public Result SerializeStatus(bool allowObjectDownloads, string serializePath)
+ {
+ // specify ignored=matching and --untracked-files=complete
+ // so the status cache can answer status commands run by Visual Studio
+ // or tools with similar requirements.
+ return this.InvokeGitInWorkingDirectoryRoot(
+ string.Format("--no-optional-locks status \"--serialize={0}\" --ignored=matching --untracked-files=complete", serializePath),
+ useReadObjectHook: allowObjectDownloads);
}
public Result UnpackObjects(Stream packFileStream)
diff --git a/GVFS/GVFS.Common/GitCommandLineParser.cs b/GVFS/GVFS.Common/GitCommandLineParser.cs
index 7270c9f5a7..e57305a818 100644
--- a/GVFS/GVFS.Common/GitCommandLineParser.cs
+++ b/GVFS/GVFS.Common/GitCommandLineParser.cs
@@ -57,6 +57,12 @@ public bool IsResetSoftOrMixed()
!this.HasArgument("--merge");
}
+ public bool IsSerializedStatus()
+ {
+ return this.IsVerb(Verbs.Status) &&
+ this.HasArgumentPrefix("--serialize");
+ }
+
///
/// This method currently just makes a best effort to detect file paths. Only use this method for optional optimizations
/// related to file paths. Do NOT use this method if you require a reliable answer.
@@ -126,6 +132,11 @@ private bool HasArgument(string argument)
return this.HasAnyArgument(arg => arg == argument);
}
+ private bool HasArgumentPrefix(string argument)
+ {
+ return this.HasAnyArgument(arg => arg.StartsWith(argument, StringComparison.Ordinal));
+ }
+
private bool HasArgumentAtIndex(string argument, int argumentIndex)
{
int actualIndex = argumentIndex + ArgumentsOffset;
diff --git a/GVFS/GVFS.Common/GitStatusCache.cs b/GVFS/GVFS.Common/GitStatusCache.cs
new file mode 100644
index 0000000000..ae7d2e18f6
--- /dev/null
+++ b/GVFS/GVFS.Common/GitStatusCache.cs
@@ -0,0 +1,560 @@
+using GVFS.Common.Git;
+using GVFS.Common.NamedPipes;
+using GVFS.Common.Tracing;
+using System;
+using System.ComponentModel;
+using System.Diagnostics;
+using System.IO;
+using System.Threading;
+using System.Threading.Tasks;
+
+namespace GVFS.Common
+{
+ ///
+ /// Responsible for orchestrating the Git Status Cache interactions. This is a cache of the results of running
+ /// the "git status" command.
+ ///
+ /// Consumers are responsible for invalidating the cache and directing it to rebuild.
+ ///
+ public class GitStatusCache : IDisposable
+ {
+ private const string EtwArea = nameof(GitStatusCache);
+ private const int DelayBeforeRunningLoopAgainMs = 1000;
+
+ private readonly TimeSpan backoffTime;
+
+ private string serializedGitStatusFilePath;
+
+ ///
+ /// The last time that the refresh loop noticed an
+ /// invalidation.
+ ///
+ private DateTime lastInvalidationTime = DateTime.MinValue;
+
+ ///
+ /// This is the time the GitStatusCache started delaying refreshes.
+ ///
+ private DateTime initialDelayTime = DateTime.MinValue;
+
+ private GVFSContext context;
+
+ private AutoResetEvent wakeUpThread;
+ private Task updateStatusCacheThread;
+ private bool isStopping;
+ private bool isInitialized;
+ private StatusStatistics statistics;
+
+ private volatile CacheState cacheState = CacheState.Dirty;
+
+ private object cacheFileLock = new object();
+
+ public GitStatusCache(GVFSContext context, GitStatusCacheConfig config)
+ : this(context, config.BackoffTime)
+ {
+ }
+
+ public GitStatusCache(GVFSContext context, TimeSpan backoffTime)
+ {
+ this.context = context;
+ this.backoffTime = backoffTime;
+ this.serializedGitStatusFilePath = this.context.Enlistment.GitStatusCachePath;
+ this.statistics = new StatusStatistics();
+
+ this.wakeUpThread = new AutoResetEvent(false);
+ }
+
+ public virtual void Initialize()
+ {
+ this.isInitialized = true;
+ this.updateStatusCacheThread = Task.Factory.StartNew(this.SerializeStatusMainThread, TaskCreationOptions.LongRunning);
+ this.Invalidate();
+ }
+
+ public virtual void Shutdown()
+ {
+ this.isStopping = true;
+
+ if (this.isInitialized && this.updateStatusCacheThread != null)
+ {
+ this.wakeUpThread.Set();
+ this.updateStatusCacheThread.Wait();
+ }
+ }
+
+ ///
+ /// Invalidate the status cache. Does not cause the cache to refresh
+ /// If caller also wants to signal the refresh, they must call
+ /// .
+ ///
+ public virtual void Invalidate()
+ {
+ this.lastInvalidationTime = DateTime.UtcNow;
+ this.cacheState = CacheState.Dirty;
+ }
+
+ public virtual bool IsCacheReadyAndUpToDate()
+ {
+ return this.cacheState == CacheState.Clean;
+ }
+
+ public virtual void RefreshAsynchronously()
+ {
+ this.wakeUpThread.Set();
+ }
+
+ public void RefreshAndWait()
+ {
+ this.RebuildStatusCacheIfNeeded(ignoreBackoff: true);
+ }
+
+ ///
+ /// The GitStatusCache gets a chance to approve / deny requests for a
+ /// command to take the GVFS lock. The GitStatusCache will only block
+ /// if the command is a status command and there is a blocking error
+ /// that might affect the correctness of the result.
+ ///
+ public virtual bool IsReadyForExternalAcquireLockRequests(
+ NamedPipeMessages.LockData requester,
+ out string infoMessage)
+ {
+ infoMessage = null;
+ if (!this.isInitialized)
+ {
+ return true;
+ }
+
+ GitCommandLineParser gitCommand = new GitCommandLineParser(requester.ParsedCommand);
+ if (!gitCommand.IsVerb(GitCommandLineParser.Verbs.Status) ||
+ gitCommand.IsSerializedStatus())
+ {
+ return true;
+ }
+
+ bool shouldAllowExternalRequest = true;
+ bool isCacheReady = false;
+
+ lock (this.cacheFileLock)
+ {
+ if (this.IsCacheReadyAndUpToDate())
+ {
+ isCacheReady = true;
+ }
+ else
+ {
+ if (!this.TryDeleteStatusCacheFile())
+ {
+ shouldAllowExternalRequest = false;
+ infoMessage = string.Format("Unable to delete stale status cache file at: {0}", this.serializedGitStatusFilePath);
+ }
+ }
+ }
+
+ if (isCacheReady)
+ {
+ this.statistics.RecordCacheReady();
+ }
+ else
+ {
+ this.statistics.RecordCacheNotReady();
+ }
+
+ if (!shouldAllowExternalRequest)
+ {
+ this.statistics.RecordBlockedRequest();
+ }
+
+ this.context.Tracer.RelatedInfo("GitStatusCache.IsReadyForExternalAcquireLockRequests: isCacheReady: {0}, shouldAllowRequest: {1}", isCacheReady, shouldAllowExternalRequest);
+
+ return shouldAllowExternalRequest;
+ }
+
+ public virtual void Dispose()
+ {
+ this.Shutdown();
+
+ if (this.wakeUpThread != null)
+ {
+ this.wakeUpThread.Dispose();
+ this.wakeUpThread = null;
+ }
+
+ if (this.updateStatusCacheThread != null)
+ {
+ this.updateStatusCacheThread.Dispose();
+ this.updateStatusCacheThread = null;
+ }
+ }
+
+ public virtual bool WriteTelemetryandReset(EventMetadata metadata)
+ {
+ bool wroteTelemetry = false;
+ if (!this.isInitialized)
+ {
+ return wroteTelemetry;
+ }
+
+ StatusStatistics statusStatistics = Interlocked.Exchange(ref this.statistics, new StatusStatistics());
+
+ if (statusStatistics.BackgroundStatusScanCount > 0)
+ {
+ wroteTelemetry = true;
+ metadata.Add("GitStatusCache.StatusScanCount", statusStatistics.BackgroundStatusScanCount);
+ }
+
+ if (statusStatistics.BackgroundStatusScanErrorCount > 0)
+ {
+ wroteTelemetry = true;
+ metadata.Add("GitStatusCache.StatusScanErrorCount", statusStatistics.BackgroundStatusScanErrorCount);
+ }
+
+ if (statusStatistics.CacheReadyCount > 0)
+ {
+ wroteTelemetry = true;
+ metadata.Add("GitStatusCache.CacheReadyCount", statusStatistics.CacheReadyCount);
+ }
+
+ if (statusStatistics.CacheNotReadyCount > 0)
+ {
+ wroteTelemetry = true;
+ metadata.Add("GitStatusCache.CacheNotReadyCount", statusStatistics.CacheNotReadyCount);
+ }
+
+ if (statusStatistics.BlockedRequestCount > 0)
+ {
+ wroteTelemetry = true;
+ metadata.Add("GitStatusCache.BlockedRequestCount", statusStatistics.BlockedRequestCount);
+ }
+
+ return wroteTelemetry;
+ }
+
+ private void SerializeStatusMainThread()
+ {
+ while (true)
+ {
+ try
+ {
+ this.wakeUpThread.WaitOne();
+
+ if (this.isStopping)
+ {
+ break;
+ }
+
+ this.RebuildStatusCacheIfNeeded(ignoreBackoff: false);
+
+ // Delay to throttle the rate of how often status is run.
+ // Do not run status again for at least this timeout.
+ Thread.Sleep(DelayBeforeRunningLoopAgainMs);
+ }
+ catch (Exception ex)
+ {
+ EventMetadata metadata = new EventMetadata();
+ metadata.Add("Area", EtwArea);
+ if (ex != null)
+ {
+ metadata.Add("Exception", ex.ToString());
+ }
+
+ this.context.Tracer.RelatedError(metadata, "Unhandled exception encountered on GitStatusCache background thread.");
+ Environment.Exit(1);
+ }
+ }
+ }
+
+ private void RebuildStatusCacheIfNeeded(bool ignoreBackoff)
+ {
+ bool needToRebuild = false;
+ DateTime now;
+
+ lock (this.cacheFileLock)
+ {
+ CacheState cacheState = this.cacheState;
+ now = DateTime.UtcNow;
+
+ if (cacheState == CacheState.Clean)
+ {
+ this.context.Tracer.RelatedInfo("GitStatusCache.RebuildStatusCacheIfNeeded: Status Cache up-to-date.");
+ }
+ else if (!this.TryDeleteStatusCacheFile())
+ {
+ // The cache is dirty, but we failed to delete the previous on disk cache.
+ // Do not rebuild the cache this time. Wait for the next invalidation
+ // to cause the thread to run again, or the on-disk cache will be deleted
+ // if a status command is run.
+ }
+ else if (!ignoreBackoff &&
+ (now - this.lastInvalidationTime) < this.backoffTime)
+ {
+ // The approriate backoff time has not elapsed yet,
+ // If this is the 1st time we are delaying the background
+ // status scan (indicated by the initialDelayTime being set to
+ // DateTime.MinValue), mark the current time. We can then track
+ // how long the scan was delayed for.
+ if (this.initialDelayTime == DateTime.MinValue)
+ {
+ this.initialDelayTime = now;
+ }
+
+ // Signal the background thread to run again, so it
+ // can check if the backoff time has elapsed and it should
+ // rebuild the status cache.
+ this.wakeUpThread.Set();
+ }
+ else
+ {
+ // The cache is dirty, and we succeeded in deleting the previous on disk cache and the minimum
+ // backoff time has passed, so now we can rebuild the status cache.
+ needToRebuild = true;
+ }
+ }
+
+ if (needToRebuild)
+ {
+ if (this.initialDelayTime > DateTime.MinValue)
+ {
+ this.context.Tracer.RelatedInfo("GitStatusCache.RebuildStatusCacheIfNeeded: Generating new Status Cache... Status scan was delayed for: {0:0.##}s", (now - this.initialDelayTime).TotalSeconds);
+ }
+ else
+ {
+ this.context.Tracer.RelatedInfo("GitStatusCache.RebuildStatusCacheIfNeeded: Generating new Status Cache...");
+ }
+
+ this.statistics.RecordBackgroundStatusScanRun();
+
+ bool rebuildStatusCacheSucceeded = this.TryRebuildStatusCache();
+
+ this.context.Tracer.RelatedInfo("GitStatusCache.RebuildStatusCacheIfNeeded: Done generating status. Cache is now: {0}", this.cacheState);
+
+ this.initialDelayTime = DateTime.MinValue;
+ }
+ }
+
+ ///
+ /// Rebuild the status cache. This will run the background status to
+ /// generate status results, and update the serialized status cache
+ /// file.
+ ///
+ private bool TryRebuildStatusCache()
+ {
+ this.context.FileSystem.CreateDirectory(this.context.Enlistment.GitStatusCacheFolder);
+
+ // The status cache is regenerated on mount. This means that even if the write to temp file
+ // and rename operation doesn't complete (due to a system crash), and there is a torn write,
+ // GVFS is still protected because a new status cache file will be generated on mount.
+ string tmpStatusFilePath = Path.Combine(this.context.Enlistment.GitStatusCacheFolder, Path.GetRandomFileName() + "_status.tmp");
+
+ GitProcess.Result statusResult = null;
+
+ // Do not modify this block unless you completely understand the comments and code within
+ {
+ // We MUST set the state to Rebuilding _immediately before_ we call the `git status` command. That allows us to
+ // check afterwards if anything happened during the status command that should invalidate the cache, and we
+ // can discard its results if that happens.
+ this.cacheState = CacheState.Rebuilding;
+
+ GitProcess git = this.context.Enlistment.CreateGitProcess();
+ statusResult = git.SerializeStatus(
+ allowObjectDownloads: true,
+ serializePath: tmpStatusFilePath);
+ }
+
+ bool rebuildSucceeded = false;
+ if (!statusResult.HasErrors)
+ {
+ lock (this.cacheFileLock)
+ {
+ // Only update the cache if our state is still Rebuilding. Otherwise, this indicates that another call
+ // to Invalidate came in, and moved the state back to Dirty.
+ if (this.cacheState == CacheState.Rebuilding)
+ {
+ rebuildSucceeded = this.MoveCacheFileToFinalLocation(tmpStatusFilePath);
+ if (rebuildSucceeded)
+ {
+ // We have to check the state once again, because it could have been invalidated while we were
+ // copying the file in the previous step. Here we do it as a CompareExchange to minimize any further races.
+ if (Interlocked.CompareExchange(ref this.cacheState, CacheState.Clean, CacheState.Rebuilding) != CacheState.Rebuilding)
+ {
+ // We did not succeed in setting the state to Clean. Note that we have already overwritten the on disk cache,
+ // but all users of the cache file first check the cacheState, and since the cacheState is not Clean, no one
+ // should ever read it.
+
+ rebuildSucceeded = false;
+ }
+ }
+
+ if (!rebuildSucceeded)
+ {
+ this.cacheState = CacheState.Dirty;
+ }
+ }
+ }
+
+ if (!rebuildSucceeded)
+ {
+ try
+ {
+ this.context.FileSystem.DeleteFile(tmpStatusFilePath);
+ }
+ catch (Exception ex) when (ex is IOException || ex is UnauthorizedAccessException)
+ {
+ EventMetadata metadata = new EventMetadata();
+ metadata.Add("Area", EtwArea);
+ metadata.Add("Exception", ex.ToString());
+
+ this.context.Tracer.RelatedError(
+ metadata,
+ string.Format("GitStatusCache is unable to delete temporary status cache file at {0}.", tmpStatusFilePath));
+ }
+ }
+ }
+ else
+ {
+ this.statistics.RecordBackgroundStatusScanError();
+ this.context.Tracer.RelatedInfo("GitStatusCache.TryRebuildStatusCache: Error generating status: {0}", statusResult.Errors);
+ }
+
+ return rebuildSucceeded;
+ }
+
+ private bool TryDeleteStatusCacheFile()
+ {
+ Debug.Assert(Monitor.IsEntered(this.cacheFileLock), "Attempting to delete the git status cache file without the cacheFileLock");
+
+ try
+ {
+ if (this.context.FileSystem.FileExists(this.serializedGitStatusFilePath))
+ {
+ this.context.FileSystem.DeleteFile(this.serializedGitStatusFilePath);
+ }
+ }
+ catch (IOException ex) when (ex is FileNotFoundException || ex is DirectoryNotFoundException)
+ {
+ // Unexpected, but maybe something deleted the file out from underneath us...
+ // As the file is deleted, lets continue with the status generation..
+ }
+ catch (Exception ex) when (ex is IOException || ex is UnauthorizedAccessException)
+ {
+ EventMetadata metadata = new EventMetadata();
+ metadata.Add("Area", EtwArea);
+ metadata.Add("Exception", ex.ToString());
+
+ this.context.Tracer.RelatedError(
+ metadata,
+ string.Format("GitStatusCache encountered exception attempting to delete cache file at {0}.", this.serializedGitStatusFilePath));
+
+ return false;
+ }
+
+ return true;
+ }
+
+ ///
+ /// Move (and overwrite) status cache file from the temporary location to the
+ /// expected location for the status cache file.
+ ///
+ /// True on success, False on failure
+ private bool MoveCacheFileToFinalLocation(string tmpStatusFilePath)
+ {
+ Debug.Assert(Monitor.IsEntered(this.cacheFileLock), "Attempting to update the git status cache file without the cacheFileLock");
+
+ try
+ {
+ this.context.FileSystem.MoveAndOverwriteFile(tmpStatusFilePath, this.serializedGitStatusFilePath);
+ return true;
+ }
+ catch (Exception ex) when (ex is IOException || ex is UnauthorizedAccessException || ex is Win32Exception)
+ {
+ EventMetadata metadata = new EventMetadata();
+ metadata.Add("Area", EtwArea);
+ metadata.Add("Exception", ex.ToString());
+
+ this.context.Tracer.RelatedError(
+ metadata,
+ string.Format("GitStatusCache encountered exception attempting to update status cache file at {0} with {1}.", this.serializedGitStatusFilePath, tmpStatusFilePath));
+ }
+
+ return false;
+ }
+
+ private class StatusStatistics
+ {
+ public int BackgroundStatusScanCount { get; private set; }
+
+ public int BackgroundStatusScanErrorCount { get; private set; }
+
+ public int CacheReadyCount { get; private set; }
+
+ public int CacheNotReadyCount { get; private set; }
+
+ public int BlockedRequestCount { get; private set; }
+
+ ///
+ /// Record that a background status scan was run. This is the
+ /// status command that is run to populate the status cache.
+ ///
+ public void RecordBackgroundStatusScanRun()
+ {
+ this.BackgroundStatusScanCount++;
+ }
+
+ ///
+ /// Record that an error was encountered while running
+ /// the background status scan.
+ ///
+ public void RecordBackgroundStatusScanError()
+ {
+ this.BackgroundStatusScanErrorCount++;
+ }
+
+ ///
+ /// Record that a status command was run from the repository,
+ /// and the cache was not ready to answer it.
+ ///
+ public void RecordCacheNotReady()
+ {
+ this.CacheNotReadyCount++;
+ }
+
+ ///
+ /// Record that a status command was run from the repository,
+ /// and the cache was ready to answer it.
+ ///
+ public void RecordCacheReady()
+ {
+ this.CacheReadyCount++;
+ }
+
+ ///
+ /// Record that a status command was run from the repository,
+ /// and the cache blocked the request. This only happens
+ /// if there is a stale status cache file and it cannot be deleted.
+ ///
+ public void RecordBlockedRequest()
+ {
+ this.BlockedRequestCount++;
+ }
+ }
+
+ // This should really be an enum, but because we need to CompareExchange it,
+ // we have to create a reference type that looks like an enum instead.
+ private class CacheState
+ {
+ public static readonly CacheState Dirty = new CacheState("Dirty");
+ public static readonly CacheState Clean = new CacheState("Clean");
+ public static readonly CacheState Rebuilding = new CacheState("Rebuilding");
+
+ private string name;
+
+ private CacheState(string name)
+ {
+ this.name = name;
+ }
+
+ public override string ToString()
+ {
+ return this.name;
+ }
+ }
+ }
+}
diff --git a/GVFS/GVFS.Common/GitStatusCacheConfig.cs b/GVFS/GVFS.Common/GitStatusCacheConfig.cs
new file mode 100644
index 0000000000..eb05638a8f
--- /dev/null
+++ b/GVFS/GVFS.Common/GitStatusCacheConfig.cs
@@ -0,0 +1,123 @@
+using GVFS.Common.Git;
+using GVFS.Common.Tracing;
+using System;
+using System.Linq;
+
+namespace GVFS.Common
+{
+ ///
+ /// Manage the reading of GitStatusCache configuration data from git config.
+ ///
+ public class GitStatusCacheConfig
+ {
+ private const string EtwArea = nameof(GitStatusCacheConfig);
+
+ private static readonly TimeSpan DefaultBackoffTime = TimeSpan.FromSeconds(2);
+
+ public GitStatusCacheConfig(TimeSpan backOffTime)
+ {
+ this.BackoffTime = backOffTime;
+ }
+
+ public static GitStatusCacheConfig DefaultConfig { get; } = new GitStatusCacheConfig(DefaultBackoffTime);
+
+ public TimeSpan BackoffTime { get; private set; }
+
+ public static bool TryLoadFromGitConfig(ITracer tracer, Enlistment enlistment, out GitStatusCacheConfig gitStatusCacheConfig, out string error)
+ {
+ return TryLoadFromGitConfig(tracer, new GitProcess(enlistment), out gitStatusCacheConfig, out error);
+ }
+
+ public static bool TryLoadFromGitConfig(ITracer tracer, GitProcess git, out GitStatusCacheConfig gitStatusCacheConfig, out string error)
+ {
+ gitStatusCacheConfig = DefaultConfig;
+
+ int backOffTimeSeconds = (int)DefaultBackoffTime.TotalSeconds;
+ if (!TryLoadBackOffTime(git, out backOffTimeSeconds, out error))
+ {
+ if (tracer != null)
+ {
+ tracer.RelatedError(
+ new EventMetadata
+ {
+ { "Area", EtwArea },
+ { "error", error }
+ },
+ $"{nameof(GitStatusCacheConfig.TryLoadFromGitConfig)}: TryLoadBackOffTime failed");
+ }
+
+ return false;
+ }
+
+ gitStatusCacheConfig = new GitStatusCacheConfig(TimeSpan.FromSeconds(backOffTimeSeconds));
+
+ if (tracer != null)
+ {
+ tracer.RelatedEvent(
+ EventLevel.Informational,
+ "GitStatusCacheConfig_Loaded",
+ new EventMetadata
+ {
+ { "Area", EtwArea },
+ { "BackOffTime", gitStatusCacheConfig.BackoffTime },
+ { TracingConstants.MessageKey.InfoMessage, "GitStatusCacheConfigLoaded" }
+ });
+ }
+
+ return true;
+ }
+
+ private static bool TryLoadBackOffTime(GitProcess git, out int backoffTimeSeconds, out string error)
+ {
+ bool returnVal = TryGetFromGitConfig(
+ git: git,
+ configName: GVFSConstants.GitConfig.GitStatusCacheBackoffConfig,
+ defaultValue: (int)DefaultBackoffTime.TotalSeconds,
+ minValue: 0,
+ value: out backoffTimeSeconds,
+ error: out error);
+
+ return returnVal;
+ }
+
+ private static bool TryGetFromGitConfig(GitProcess git, string configName, int defaultValue, int minValue, out int value, out string error)
+ {
+ value = defaultValue;
+ error = string.Empty;
+
+ GitProcess.Result result = git.GetFromConfig(configName);
+ if (result.HasErrors)
+ {
+ if (result.Errors.Any())
+ {
+ error = "Error while reading '" + configName + "' from config: " + result.Errors;
+ return false;
+ }
+
+ // Git returns non-zero for non-existent settings and errors.
+ return true;
+ }
+
+ string valueString = result.Output.TrimEnd('\n');
+ if (string.IsNullOrWhiteSpace(valueString))
+ {
+ // Use default value
+ return true;
+ }
+
+ if (!int.TryParse(valueString, out value))
+ {
+ error = string.Format("Misconfigured config setting {0}, could not parse value {1}", configName, valueString);
+ return false;
+ }
+
+ if (value < minValue)
+ {
+ error = string.Format("Invalid value {0} for setting {1}, value must be greater than or equal to {2}", value, configName, minValue);
+ return false;
+ }
+
+ return true;
+ }
+ }
+}
diff --git a/GVFS/GVFS.Common/Http/CacheServerInfo.cs b/GVFS/GVFS.Common/Http/CacheServerInfo.cs
index cd1dc173f2..4fe1e58c63 100644
--- a/GVFS/GVFS.Common/Http/CacheServerInfo.cs
+++ b/GVFS/GVFS.Common/Http/CacheServerInfo.cs
@@ -37,6 +37,12 @@ public bool HasValidUrl()
return Uri.IsWellFormedUriString(this.Url, UriKind.Absolute);
}
+ public bool IsNone(string repoUrl)
+ {
+ return ReservedNames.None.Equals(this.Name, StringComparison.OrdinalIgnoreCase)
+ || this.Url?.StartsWith(repoUrl, StringComparison.OrdinalIgnoreCase) == true;
+ }
+
public override string ToString()
{
if (string.IsNullOrWhiteSpace(this.Name))
diff --git a/GVFS/GVFS.Common/NativeMethods.cs b/GVFS/GVFS.Common/NativeMethods.cs
index 7d9bde70b9..c4564e44d4 100644
--- a/GVFS/GVFS.Common/NativeMethods.cs
+++ b/GVFS/GVFS.Common/NativeMethods.cs
@@ -155,6 +155,15 @@ public static bool IsSymlink(string path)
}
}
+ public static DateTime GetLastRebootTime()
+ {
+ // GetTickCount64 is a native call and returns the number
+ // of milliseconds since the system was started (and not DateTime.Ticks).
+ // https://msdn.microsoft.com/en-us/library/windows/desktop/ms724411.aspx
+ TimeSpan uptime = TimeSpan.FromMilliseconds(GetTickCount64());
+ return DateTime.Now - uptime;
+ }
+
[DllImport("kernel32.dll", SetLastError = true, CharSet = CharSet.Unicode)]
private static extern bool MoveFileEx(
string existingFileName,
@@ -203,6 +212,9 @@ private static extern bool DeviceIoControl(
out uint pBytesReturned,
[In] IntPtr Overlapped);
+ [DllImport("kernel32.dll")]
+ private static extern ulong GetTickCount64();
+
[StructLayout(LayoutKind.Sequential, CharSet = CharSet.Unicode)]
private struct REPARSE_DATA_BUFFER
{
diff --git a/GVFS/GVFS.Common/Prefetch/BackgroundPrefetcher.cs b/GVFS/GVFS.Common/Prefetch/BackgroundPrefetcher.cs
index e47a7e2f69..fb0a14efb6 100644
--- a/GVFS/GVFS.Common/Prefetch/BackgroundPrefetcher.cs
+++ b/GVFS/GVFS.Common/Prefetch/BackgroundPrefetcher.cs
@@ -29,7 +29,15 @@ public BackgroundPrefetcher(ITracer tracer, GVFSEnlistment enlistment, PhysicalF
this.prefetchJobThread = null;
- this.prefetchJobTimer = new Timer((state) => this.LaunchPrefetchJobIfIdle(), null, this.timerPeriod, this.timerPeriod);
+ if (gitObjects.IsUsingCacheServer())
+ {
+ this.prefetchJobTimer = new Timer((state) => this.LaunchPrefetchJobIfIdle(), null, this.timerPeriod, this.timerPeriod);
+ this.tracer.RelatedInfo(nameof(BackgroundPrefetcher) + ": starting background prefetch timer");
+ }
+ else
+ {
+ this.tracer.RelatedInfo(nameof(BackgroundPrefetcher) + ": no configured cache server, not starting background prefetch timer");
+ }
}
public void Dispose()
diff --git a/GVFS/GVFS.Common/RepoMetadata.cs b/GVFS/GVFS.Common/RepoMetadata.cs
index 13fcecfc4a..db28c223d9 100644
--- a/GVFS/GVFS.Common/RepoMetadata.cs
+++ b/GVFS/GVFS.Common/RepoMetadata.cs
@@ -308,7 +308,7 @@ public static class Keys
public const string GitObjectsRoot = "GitObjectsRoot";
public const string LocalCacheRoot = "LocalCacheRoot";
public const string BlobSizesRoot = "BlobSizesRoot";
- public const string EnlistmentId = "EnlistmentId";
+ public const string EnlistmentId = "EnlistmentId";
}
public static class DiskLayoutVersion
@@ -316,7 +316,7 @@ public static class DiskLayoutVersion
// The major version should be bumped whenever there is an on-disk format change that requires a one-way upgrade.
// Increasing this version will make older versions of GVFS unable to mount a repo that has been mounted by a newer
// version of GVFS.
- public const int CurrentMajorVersion = 15;
+ public const int CurrentMajorVersion = 16;
// The minor version should be bumped whenever there is an upgrade that can be safely ignored by older versions of GVFS.
// For example, this allows an upgrade step that sets a default value for some new config setting.
diff --git a/GVFS/GVFS.FunctionalTests.Windows/Windows/Tests/DiskLayoutUpgradeTests.cs b/GVFS/GVFS.FunctionalTests.Windows/Windows/Tests/DiskLayoutUpgradeTests.cs
index 3344fd5aae..ae4f30587e 100644
--- a/GVFS/GVFS.FunctionalTests.Windows/Windows/Tests/DiskLayoutUpgradeTests.cs
+++ b/GVFS/GVFS.FunctionalTests.Windows/Windows/Tests/DiskLayoutUpgradeTests.cs
@@ -17,7 +17,7 @@ namespace GVFS.FunctionalTests.Windows.Tests
[Category(Categories.Windows)]
public class DiskLayoutUpgradeTests : TestsWithEnlistmentPerTestCase
{
- public const int CurrentDiskLayoutMajorVersion = 15;
+ public const int CurrentDiskLayoutMajorVersion = 16;
public const int CurrentDiskLayoutMinorVersion = 0;
public const string BlobSizesCacheName = "blobSizes";
@@ -341,11 +341,12 @@ private string[] GetPlaceholderDatabaseLinesBeforeUpgrade(string placeholderData
{
placeholderDatabasePath.ShouldBeAFile(this.fileSystem);
string[] lines = this.fileSystem.ReadAllText(placeholderDatabasePath).Split(new char[] { '\r', '\n' }, StringSplitOptions.RemoveEmptyEntries);
- lines.Length.ShouldEqual(11);
+ lines.Length.ShouldEqual(12);
lines.ShouldContain(x => x.Contains("Readme.md"));
lines.ShouldContain(x => x.Contains("Scripts\\RunUnitTests.bat"));
lines.ShouldContain(x => x.Contains("GVFS\\GVFS.Common\\Git\\GitRefs.cs"));
lines.ShouldContain(x => x.Contains("A GVFS\\GVFS.Tests\\Properties\\AssemblyInfo.cs"));
+ lines.ShouldContain(x => x.Contains("A .gitignore"));
lines.ShouldContain(x => x == "D GVFS\\GVFS.Tests\\Properties\\AssemblyInfo.cs");
lines.ShouldContain(x => x == "A Scripts\0" + TestConstants.AllZeroSha);
lines.ShouldContain(x => x == "A GVFS\0" + TestConstants.AllZeroSha);
@@ -360,10 +361,11 @@ private string[] GetPlaceholderDatabaseLinesAfterUpgrade(string placeholderDatab
{
placeholderDatabasePath.ShouldBeAFile(this.fileSystem);
string[] lines = this.fileSystem.ReadAllText(placeholderDatabasePath).Split(new char[] { '\r', '\n' }, StringSplitOptions.RemoveEmptyEntries);
- lines.Length.ShouldEqual(8);
+ lines.Length.ShouldEqual(9);
lines.ShouldContain(x => x.Contains("Readme.md"));
lines.ShouldContain(x => x.Contains("Scripts\\RunUnitTests.bat"));
lines.ShouldContain(x => x.Contains("GVFS\\GVFS.Common\\Git\\GitRefs.cs"));
+ lines.ShouldContain(x => x.Contains("A .gitignore"));
lines.ShouldContain(x => x == "A Scripts\0" + TestConstants.AllZeroSha);
lines.ShouldContain(x => x == "A GVFS\0" + TestConstants.AllZeroSha);
lines.ShouldContain(x => x == "A GVFS\\GVFS.Common\0" + TestConstants.AllZeroSha);
diff --git a/GVFS/GVFS.FunctionalTests/Program.cs b/GVFS/GVFS.FunctionalTests/Program.cs
index b1f4baca8f..a99040e471 100644
--- a/GVFS/GVFS.FunctionalTests/Program.cs
+++ b/GVFS/GVFS.FunctionalTests/Program.cs
@@ -5,8 +5,8 @@
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
-using System.Runtime.InteropServices;
-
+using System.Runtime.InteropServices;
+
namespace GVFS.FunctionalTests
{
public class Program
@@ -90,32 +90,43 @@ public static void Main(string[] args)
private static void RunBeforeAnyTests()
{
if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
- {
- if (GVFSTestConfig.ReplaceInboxProjFS)
- {
- ProjFSFilterInstaller.ReplaceInboxProjFS();
- }
-
+ {
+ if (GVFSTestConfig.ReplaceInboxProjFS)
+ {
+ ProjFSFilterInstaller.ReplaceInboxProjFS();
+ }
+
GVFSServiceProcess.InstallService();
+
+ string statusCacheVersionTokenPath = Path.Combine(
+ Environment.GetFolderPath(Environment.SpecialFolder.CommonApplicationData, Environment.SpecialFolderOption.Create),
+ "GVFS",
+ "GVFS.Service",
+ "EnableGitStatusCacheToken.dat");
+
+ if (!File.Exists(statusCacheVersionTokenPath))
+ {
+ File.WriteAllText(statusCacheVersionTokenPath, string.Empty);
+ }
}
}
private static void RunAfterAllTests()
{
if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
- {
- string serviceLogFolder = Path.Combine(
- Environment.GetFolderPath(Environment.SpecialFolder.CommonApplicationData),
- "GVFS",
- GVFSServiceProcess.TestServiceName,
- "Logs");
-
- Console.WriteLine("GVFS.Service logs at '{0}' attached below.\n\n", serviceLogFolder);
- foreach (string filename in TestResultsHelper.GetAllFilesInDirectory(serviceLogFolder))
- {
- TestResultsHelper.OutputFileContents(filename);
- }
-
+ {
+ string serviceLogFolder = Path.Combine(
+ Environment.GetFolderPath(Environment.SpecialFolder.CommonApplicationData),
+ "GVFS",
+ GVFSServiceProcess.TestServiceName,
+ "Logs");
+
+ Console.WriteLine("GVFS.Service logs at '{0}' attached below.\n\n", serviceLogFolder);
+ foreach (string filename in TestResultsHelper.GetAllFilesInDirectory(serviceLogFolder))
+ {
+ TestResultsHelper.OutputFileContents(filename);
+ }
+
GVFSServiceProcess.UninstallService();
}
diff --git a/GVFS/GVFS.FunctionalTests/Tests/EnlistmentPerFixture/UpdatePlaceholderTests.cs b/GVFS/GVFS.FunctionalTests/Tests/EnlistmentPerFixture/UpdatePlaceholderTests.cs
index 075adb800a..22146b1aef 100644
--- a/GVFS/GVFS.FunctionalTests/Tests/EnlistmentPerFixture/UpdatePlaceholderTests.cs
+++ b/GVFS/GVFS.FunctionalTests/Tests/EnlistmentPerFixture/UpdatePlaceholderTests.cs
@@ -16,8 +16,8 @@ namespace GVFS.FunctionalTests.Tests.EnlistmentPerFixture
public class UpdatePlaceholderTests : TestsWithEnlistmentPerFixture
{
private const string TestParentFolderName = "Test_EPF_UpdatePlaceholderTests";
- private const string OldCommitId = "7bb7945e4767b43174c7468828b0eaf39bd2f110";
- private const string NewFilesAndChangesCommitId = "b4d932658def04a97da873fd6adab70014b8a523";
+ private const string OldCommitId = "5d7a7d4db1734fb468a4094469ec58d26301b59d";
+ private const string NewFilesAndChangesCommitId = "fec239ea12de1eda6ae5329d4f345784d5b61ff9";
private FileSystemRunner fileSystem;
public UpdatePlaceholderTests()
diff --git a/GVFS/GVFS.FunctionalTests/Tests/EnlistmentPerFixture/WorkingDirectoryTests.cs b/GVFS/GVFS.FunctionalTests/Tests/EnlistmentPerFixture/WorkingDirectoryTests.cs
index d765e69bbf..35470fd4f8 100644
--- a/GVFS/GVFS.FunctionalTests/Tests/EnlistmentPerFixture/WorkingDirectoryTests.cs
+++ b/GVFS/GVFS.FunctionalTests/Tests/EnlistmentPerFixture/WorkingDirectoryTests.cs
@@ -371,7 +371,7 @@ public void WriteToHydratedFileAfterRemount()
[TestCase, Order(10)]
public void ReadDeepProjectedFile()
- {
+ {
string testFilePath = Path.Combine("Test_EPF_WorkingDirectoryTests", "1", "2", "3", "4", "ReadDeepProjectedFile.cpp");
this.Enlistment.GetVirtualPathTo(testFilePath).ShouldBeAFile(this.fileSystem).WithContents(TestFileContents);
}
@@ -407,8 +407,8 @@ public void FolderContentsProjectedAfterFolderCreateAndCheckout()
{
string folderName = "GVFlt_MultiThreadTest";
- // 575d597cf09b2cd1c0ddb4db21ce96979010bbcb did not have the folder GVFlt_MultiThreadTest
- GitProcess.InvokeProcess(this.Enlistment.RepoRoot, "checkout 575d597cf09b2cd1c0ddb4db21ce96979010bbcb");
+ // 54ea499de78eafb4dfd30b90e0bd4bcec26c4349 did not have the folder GVFlt_MultiThreadTest
+ GitProcess.InvokeProcess(this.Enlistment.RepoRoot, "checkout 54ea499de78eafb4dfd30b90e0bd4bcec26c4349");
// Confirm that no other test has created GVFlt_MultiThreadTest or put it in the modified files
GVFSHelpers.ModifiedPathsShouldNotContain(this.fileSystem, this.Enlistment.DotGVFSRoot, folderName);
@@ -417,10 +417,10 @@ public void FolderContentsProjectedAfterFolderCreateAndCheckout()
virtualFolderPath.ShouldNotExistOnDisk(this.fileSystem);
this.fileSystem.CreateDirectory(virtualFolderPath);
- // b5fd7d23706a18cff3e2b8225588d479f7e51138 was the commit prior to deleting GVFLT_MultiThreadTest
+ // b3ddcf43b997cba3fbf9d2341b297e22bf48601a was the commit prior to deleting GVFLT_MultiThreadTest
// 692765: Note that test also validates case insensitivity as GVFlt_MultiThreadTest is named GVFLT_MultiThreadTest
// in this commit
- GitProcess.InvokeProcess(this.Enlistment.RepoRoot, "checkout b5fd7d23706a18cff3e2b8225588d479f7e51138");
+ GitProcess.InvokeProcess(this.Enlistment.RepoRoot, "checkout b3ddcf43b997cba3fbf9d2341b297e22bf48601a");
this.Enlistment.GetVirtualPathTo(folderName + "\\OpenForReadsSameTime\\test").ShouldBeAFile(this.fileSystem).WithContents("123 \r\n");
this.Enlistment.GetVirtualPathTo(folderName + "\\OpenForWritesSameTime\\test").ShouldBeAFile(this.fileSystem).WithContents("123 \r\n");
@@ -431,8 +431,8 @@ public void FolderContentsProjectedAfterFolderCreateAndCheckout()
[Category(Categories.Mac.M3)]
public void FolderContentsCorrectAfterCreateNewFolderRenameAndCheckoutCommitWithSameFolder()
{
- // 1ca414ced40f64bf94fc6c7f885974708bc600be is the commit prior to adding Test_EPF_MoveRenameFileTests
- GitProcess.InvokeProcess(this.Enlistment.RepoRoot, "checkout 1ca414ced40f64bf94fc6c7f885974708bc600be");
+ // 3a55d3b760c87642424e834228a3408796501e7c is the commit prior to adding Test_EPF_MoveRenameFileTests
+ GitProcess.InvokeProcess(this.Enlistment.RepoRoot, "checkout 3a55d3b760c87642424e834228a3408796501e7c");
// Confirm that no other test has created this folder or put it in the modified files
string folderName = "Test_EPF_MoveRenameFileTests";
@@ -457,7 +457,9 @@ public void FolderContentsCorrectAfterCreateNewFolderRenameAndCheckoutCommitWith
(folder + @"\MoveUnhydratedFileToDotGitFolder\Program.cs").ShouldBeAFile(this.fileSystem).WithContents(MoveRenameFileTests.TestFileContents);
}
+ // TODO(Mac) This test is technically part of M2, but we need further investigation of why this test fails on build agents, but not on dev machines.
[TestCase, Order(15)]
+ [Category(Categories.Mac.M3)]
public void FilterNonUTF8FileName()
{
string encodingFilename = "ريلٌأكتوبرûمارسأغسطسºٰٰۂْٗ۵ريلٌأك.txt";
@@ -592,7 +594,8 @@ private void FolderEnumerationShouldHaveSingleEntry(string folderVirtualPath, st
}
folderEntries.Count().ShouldEqual(1);
- folderEntries.ShouldContain(file => file.Name.Equals(expectedEntryName));
+ FileSystemInfo singleEntry = folderEntries.First();
+ singleEntry.Name.ShouldEqual(expectedEntryName, $"Actual name: {singleEntry.Name} does not equal expected name {expectedEntryName}");
}
private void EnumerateAndReadShouldNotChangeEnumerationOrder(string folderRelativePath)
diff --git a/GVFS/GVFS.FunctionalTests/Tests/FastFetchTests.cs b/GVFS/GVFS.FunctionalTests/Tests/FastFetchTests.cs
index e306216fca..979c72649e 100644
--- a/GVFS/GVFS.FunctionalTests/Tests/FastFetchTests.cs
+++ b/GVFS/GVFS.FunctionalTests/Tests/FastFetchTests.cs
@@ -204,8 +204,8 @@ public void IncrementalChangesLeaveGoodStatus()
{
// Specific commits taken from branch FunctionalTests/20170206_Conflict_Source
// These commits have adds, edits and removals
- const string BaseCommit = "170b13ce1990c53944403a70e93c257061598ae0";
- const string UpdateCommit = "f2546f8e9ce7d7b1e3a0835932f0d6a6145665b1";
+ const string BaseCommit = "db95d631e379d366d26d899523f8136a77441914";
+ const string UpdateCommit = "51d15f7584e81d59d44c1511ce17d7c493903390";
GitProcess.Invoke(this.fastFetchRepoRoot, "config --local --add core.gvfs 1");
@@ -262,10 +262,10 @@ public void CanDetectAlreadyUpToDate()
public void SuccessfullyChecksOutCaseChanges()
{
// The delta between these two is the same as the UnitTest "caseChange.txt" data file.
- this.RunFastFetch("--checkout -c b5fd7d23706a18cff3e2b8225588d479f7e51138");
- this.RunFastFetch("--checkout -c fd4ae4312eb504fd40e78d2d4cf349004967a8b4");
+ this.RunFastFetch("--checkout -c b3ddcf43b997cba3fbf9d2341b297e22bf48601a");
+ this.RunFastFetch("--checkout -c e637c874f6a914ae83cd5668bcdd07293fef961d");
- GitProcess.Invoke(this.fastFetchControlRoot, "checkout fd4ae4312eb504fd40e78d2d4cf349004967a8b4");
+ GitProcess.Invoke(this.fastFetchControlRoot, "checkout e637c874f6a914ae83cd5668bcdd07293fef961d");
try
{
diff --git a/GVFS/GVFS.FunctionalTests/Tests/GitCommands/CheckoutTests.cs b/GVFS/GVFS.FunctionalTests/Tests/GitCommands/CheckoutTests.cs
index b9e757bbac..e290e021ee 100644
--- a/GVFS/GVFS.FunctionalTests/Tests/GitCommands/CheckoutTests.cs
+++ b/GVFS/GVFS.FunctionalTests/Tests/GitCommands/CheckoutTests.cs
@@ -90,13 +90,13 @@ private enum NativeFileAccess : uint
[TestCase]
public void CheckoutNewBranchFromStartingPointTest()
{
- // In commit 575d597cf09b2cd1c0ddb4db21ce96979010bbcb the CheckoutNewBranchFromStartingPointTest files were not present
- this.ValidateGitCommand("checkout 575d597cf09b2cd1c0ddb4db21ce96979010bbcb");
+ // In commit 8df701986dea0a5e78b742d2eaf9348825b14d35 the CheckoutNewBranchFromStartingPointTest files were not present
+ this.ValidateGitCommand("checkout 8df701986dea0a5e78b742d2eaf9348825b14d35");
this.ShouldNotExistOnDisk("GitCommandsTests\\CheckoutNewBranchFromStartingPointTest\\test1.txt");
this.ShouldNotExistOnDisk("GitCommandsTests\\CheckoutNewBranchFromStartingPointTest\\test2.txt");
- // In commit 27cc59d3e9a996f1fdc1230c8a80553b316a1d00 the CheckoutNewBranchFromStartingPointTest files were present
- this.ValidateGitCommand("checkout -b tests/functional/CheckoutNewBranchFromStartingPointTest 27cc59d3e9a996f1fdc1230c8a80553b316a1d00");
+ // In commit cd5c55fea4d58252bb38058dd3818da75aff6685 the CheckoutNewBranchFromStartingPointTest files were present
+ this.ValidateGitCommand("checkout -b tests/functional/CheckoutNewBranchFromStartingPointTest cd5c55fea4d58252bb38058dd3818da75aff6685");
this.FileShouldHaveContents("GitCommandsTests\\CheckoutNewBranchFromStartingPointTest\\test1.txt", "TestFile1 \r\n");
this.FileShouldHaveContents("GitCommandsTests\\CheckoutNewBranchFromStartingPointTest\\test2.txt", "TestFile2 \r\n");
@@ -106,13 +106,13 @@ public void CheckoutNewBranchFromStartingPointTest()
[TestCase]
public void CheckoutOrhpanBranchFromStartingPointTest()
{
- // In commit 27cc59d3e9a996f1fdc1230c8a80553b316a1d00 the CheckoutOrhpanBranchFromStartingPointTest files were not present
- this.ValidateGitCommand("checkout 575d597cf09b2cd1c0ddb4db21ce96979010bbcb");
+ // In commit 8df701986dea0a5e78b742d2eaf9348825b14d35 the CheckoutOrhpanBranchFromStartingPointTest files were not present
+ this.ValidateGitCommand("checkout 8df701986dea0a5e78b742d2eaf9348825b14d35");
this.ShouldNotExistOnDisk("GitCommandsTests\\CheckoutOrhpanBranchFromStartingPointTest\\test1.txt");
this.ShouldNotExistOnDisk("GitCommandsTests\\CheckoutOrhpanBranchFromStartingPointTest\\test2.txt");
- // In commit eff45342f895742b7d0a812f49611334e0b5b785 the CheckoutOrhpanBranchFromStartingPointTest files were present
- this.ValidateGitCommand("checkout --orphan tests/functional/CheckoutOrhpanBranchFromStartingPointTest eff45342f895742b7d0a812f49611334e0b5b785");
+ // In commit 15a9676c9192448820bd243807f6dab1bac66680 the CheckoutOrhpanBranchFromStartingPointTest files were present
+ this.ValidateGitCommand("checkout --orphan tests/functional/CheckoutOrhpanBranchFromStartingPointTest 15a9676c9192448820bd243807f6dab1bac66680");
this.FileShouldHaveContents("GitCommandsTests\\CheckoutOrhpanBranchFromStartingPointTest\\test1.txt", "TestFile1 \r\n");
this.FileShouldHaveContents("GitCommandsTests\\CheckoutOrhpanBranchFromStartingPointTest\\test2.txt", "TestFile2 \r\n");
@@ -127,9 +127,9 @@ public void MoveFileFromDotGitFolderToWorkingDirectoryAndAddAndCheckout()
string dotGitFilePath = @".git\" + filename;
string targetPath = @"Test_ConflictTests\AddedFiles\" + filename;
- // In commit 27cc59d3e9a996f1fdc1230c8a80553b316a1d00 Test_ConflictTests\AddedFiles\AddedBySource.txt does not exist
+ // In commit db95d631e379d366d26d899523f8136a77441914 Test_ConflictTests\AddedFiles\AddedBySource.txt does not exist
this.ControlGitRepo.Fetch(GitRepoTests.ConflictSourceBranch);
- this.ValidateGitCommand("checkout 170b13ce1990c53944403a70e93c257061598ae0");
+ this.ValidateGitCommand("checkout db95d631e379d366d26d899523f8136a77441914");
string newBranchName = "tests/functional/MoveFileFromDotGitFolderToWorkingDirectoryAndAddAndCheckout";
this.ValidateGitCommand("checkout -b " + newBranchName);
@@ -146,8 +146,8 @@ public void MoveFileFromDotGitFolderToWorkingDirectoryAndAddAndCheckout()
this.ValidateGitCommand("add .");
this.RunGitCommand("commit -m \"Change for MoveFileFromDotGitFolderToWorkingDirectoryAndAddAndCheckout\"");
- // In commit f2546f8e9ce7d7b1e3a0835932f0d6a6145665b1 Test_ConflictTests\AddedFiles\AddedBySource.txt was added
- this.ValidateGitCommand("checkout f2546f8e9ce7d7b1e3a0835932f0d6a6145665b1");
+ // In commit 51d15f7584e81d59d44c1511ce17d7c493903390 Test_ConflictTests\AddedFiles\AddedBySource.txt was added
+ this.ValidateGitCommand("checkout 51d15f7584e81d59d44c1511ce17d7c493903390");
this.FileContentsShouldMatch(targetPath);
}
@@ -166,8 +166,8 @@ public void CheckoutCommitWhereFileContentsChangeAfterRead()
string fileName = "SameChange.txt";
- // In commit 170b13ce1990c53944403a70e93c257061598ae0 the initial files for the FunctionalTests/20170206_Conflict_Source branch were created
- this.ValidateGitCommand("checkout 170b13ce1990c53944403a70e93c257061598ae0");
+ // In commit db95d631e379d366d26d899523f8136a77441914 the initial files for the FunctionalTests/20170206_Conflict_Source branch were created
+ this.ValidateGitCommand("checkout db95d631e379d366d26d899523f8136a77441914");
this.FileContentsShouldMatch(@"Test_ConflictTests\ModifiedFiles\" + fileName);
// A read should not add the file to the modified paths
@@ -186,8 +186,8 @@ public void CheckoutCommitWhereFileDeletedAfterRead()
string fileName = "DeleteInSource.txt";
string filePath = @"Test_ConflictTests\DeletedFiles\" + fileName;
- // In commit 170b13ce1990c53944403a70e93c257061598ae0 the initial files for the FunctionalTests/20170206_Conflict_Source branch were created
- this.ValidateGitCommand("checkout 170b13ce1990c53944403a70e93c257061598ae0");
+ // In commit db95d631e379d366d26d899523f8136a77441914 the initial files for the FunctionalTests/20170206_Conflict_Source branch were created
+ this.ValidateGitCommand("checkout db95d631e379d366d26d899523f8136a77441914");
this.FileContentsShouldMatch(filePath);
// A read should not add the file to the modified paths
@@ -350,12 +350,12 @@ public void MarkFileAsReadOnlyAndCheckoutCommitWhereFileIsDeleted()
[TestCase]
public void ModifyAndCheckoutFirstOfSeveralFilesWhoseNamesAppearBeforeDot()
{
- // Commit 14cf226119766146b1fa5c5aa4cd0896d05f6b63 has the files (a).txt and (z).txt
+ // Commit cb2d05febf64e3b0df50bd8d3fe8f05c0e2caa47 has the files (a).txt and (z).txt
// in the DeleteFileWithNameAheadOfDotAndSwitchCommits folder
string originalContent = "Test contents for (a).txt";
string newContent = "content to append";
- this.ValidateGitCommand("checkout 14cf226119766146b1fa5c5aa4cd0896d05f6b63");
+ this.ValidateGitCommand("checkout cb2d05febf64e3b0df50bd8d3fe8f05c0e2caa47");
this.EditFile("DeleteFileWithNameAheadOfDotAndSwitchCommits\\(a).txt", newContent);
this.FileShouldHaveContents("DeleteFileWithNameAheadOfDotAndSwitchCommits\\(a).txt", originalContent + newContent);
this.ValidateGitCommand("status");
@@ -369,18 +369,18 @@ public void ResetMixedToCommitWithNewFileThenCheckoutNewBranchAndCheckoutCommitW
{
this.ControlGitRepo.Fetch(GitRepoTests.ConflictSourceBranch);
- // Commit 170b13ce1990c53944403a70e93c257061598ae0 was prior to the additional of these
- // three files in commit f2546f8e9ce7d7b1e3a0835932f0d6a6145665b1:
+ // Commit db95d631e379d366d26d899523f8136a77441914 was prior to the additional of these
+ // three files in commit 51d15f7584e81d59d44c1511ce17d7c493903390:
// Test_ConflictTests/AddedFiles/AddedByBothDifferentContent.txt
// Test_ConflictTests/AddedFiles/AddedByBothSameContent.txt
// Test_ConflictTests/AddedFiles/AddedBySource.txt
- this.ValidateGitCommand("checkout 170b13ce1990c53944403a70e93c257061598ae0");
- this.ValidateGitCommand("reset --mixed f2546f8e9ce7d7b1e3a0835932f0d6a6145665b1");
+ this.ValidateGitCommand("checkout db95d631e379d366d26d899523f8136a77441914");
+ this.ValidateGitCommand("reset --mixed 51d15f7584e81d59d44c1511ce17d7c493903390");
// Use RunGitCommand rather than ValidateGitCommand as G4W optimizations for "checkout -b" mean that the
// command will not report modified and deleted files
this.RunGitCommand("checkout -b tests/functional/ResetMixedToCommitWithNewFileThenCheckoutNewBranchAndCheckoutCommitWithNewFile");
- this.ValidateGitCommand("checkout f2546f8e9ce7d7b1e3a0835932f0d6a6145665b1");
+ this.ValidateGitCommand("checkout 51d15f7584e81d59d44c1511ce17d7c493903390");
}
// ReadFileAfterTryingToReadFileAtCommitWhereFileDoesNotExist is meant to exercise the NegativePathCache and its
@@ -390,12 +390,12 @@ public void ReadFileAfterTryingToReadFileAtCommitWhereFileDoesNotExist()
{
this.ControlGitRepo.Fetch(GitRepoTests.ConflictSourceBranch);
- // Commit 170b13ce1990c53944403a70e93c257061598ae0 was prior to the additional of these
- // three files in commit f2546f8e9ce7d7b1e3a0835932f0d6a6145665b1:
+ // Commit db95d631e379d366d26d899523f8136a77441914 was prior to the additional of these
+ // three files in commit 51d15f7584e81d59d44c1511ce17d7c493903390:
// Test_ConflictTests/AddedFiles/AddedByBothDifferentContent.txt
// Test_ConflictTests/AddedFiles/AddedByBothSameContent.txt
// Test_ConflictTests/AddedFiles/AddedBySource.txt
- this.ValidateGitCommand("checkout 170b13ce1990c53944403a70e93c257061598ae0");
+ this.ValidateGitCommand("checkout db95d631e379d366d26d899523f8136a77441914");
// Files should not exist
this.ShouldNotExistOnDisk(@"Test_ConflictTests\AddedFiles\AddedByBothDifferentContent.txt");
@@ -408,7 +408,7 @@ public void ReadFileAfterTryingToReadFileAtCommitWhereFileDoesNotExist()
this.ShouldNotExistOnDisk(@"Test_ConflictTests\AddedFiles\AddedBySource.txt");
// Switch to commit where files should exist
- this.ValidateGitCommand("checkout f2546f8e9ce7d7b1e3a0835932f0d6a6145665b1");
+ this.ValidateGitCommand("checkout 51d15f7584e81d59d44c1511ce17d7c493903390");
// Confirm files exist
this.FileContentsShouldMatch(@"Test_ConflictTests\AddedFiles\AddedByBothDifferentContent.txt");
@@ -416,7 +416,7 @@ public void ReadFileAfterTryingToReadFileAtCommitWhereFileDoesNotExist()
this.FileContentsShouldMatch(@"Test_ConflictTests\AddedFiles\AddedBySource.txt");
// Switch to commit where files should not exist
- this.ValidateGitCommand("checkout 170b13ce1990c53944403a70e93c257061598ae0");
+ this.ValidateGitCommand("checkout db95d631e379d366d26d899523f8136a77441914");
// Verify files do not not exist
this.ShouldNotExistOnDisk(@"Test_ConflictTests\AddedFiles\AddedByBothDifferentContent.txt");
@@ -654,16 +654,16 @@ public void ResetMixedTwiceThenCheckoutWithChanges()
{
this.ControlGitRepo.Fetch("FunctionalTests/20171219_MultipleFileEdits");
- this.ValidateGitCommand("checkout 2cea00f61ad600e03c35bfb8db73e4cd5827552f");
+ this.ValidateGitCommand("checkout c0ca0f00063cdc969954fa9cb92dd4abe5e095e0");
this.ValidateGitCommand("checkout -b tests/functional/ResetMixedTwice");
- // Between the original commit 2cea00f61ad600e03c35bfb8db73e4cd5827552f and the second reset
- // 437910d00a04aba7672fb011f72bc2acd92ec043, several files are changed, but none are added or
- // removed. The middle commit 0782e6e03604316a37049a6aaea5368ff582a727 includes a variety
+ // Between the original commit c0ca0f00063cdc969954fa9cb92dd4abe5e095e0 and the second reset
+ // 3ed4178bcb85085c06a24a76d2989f2364a64589, several files are changed, but none are added or
+ // removed. The middle commit 2af5f08d010eade3c73a582711a36f0def10d6bc includes a variety
// of changes including a renamed folder and new and removed files. The final checkout is
// expected to error on changed files only.
- this.ValidateGitCommand("reset --mixed 0782e6e03604316a37049a6aaea5368ff582a727");
- this.ValidateGitCommand("reset --mixed 437910d00a04aba7672fb011f72bc2acd92ec043");
+ this.ValidateGitCommand("reset --mixed 2af5f08d010eade3c73a582711a36f0def10d6bc");
+ this.ValidateGitCommand("reset --mixed 3ed4178bcb85085c06a24a76d2989f2364a64589");
this.ValidateGitCommand("checkout " + this.ControlGitRepo.Commitish);
}
@@ -673,16 +673,16 @@ public void ResetMixedTwiceThenCheckoutWithRemovedFiles()
{
this.ControlGitRepo.Fetch("FunctionalTests/20180102_MultipleFileDeletes");
- this.ValidateGitCommand("checkout d0ffd18c85e2f7aea967d9ea0287ab2677df2067");
+ this.ValidateGitCommand("checkout dee2cd6645752137e4e4eb311319bb95f533c2f1");
this.ValidateGitCommand("checkout -b tests/functional/ResetMixedTwice");
- // Between the original commit d0ffd18c85e2f7aea967d9ea0287ab2677df2067 and the second reset
- // da8aaf4cd4d13677b2a71d04d4f3c7290a8ea0da, several files are removed, but none are changed.
- // The middle commit 95de01c9d5fb413b65d8ba097ed5aecf35477515 includes a variety of changes
+ // Between the original commit dee2cd6645752137e4e4eb311319bb95f533c2f1 and the second reset
+ // 4275906774e9cc37a6875448cd3fcdc5b3ea2be3, several files are removed, but none are changed.
+ // The middle commit c272d4846f2250edfb35fcac60b4b66bb17478fa includes a variety of changes
// including a renamed folder as well as new, removed and changed files. The final checkout
// is expected to error on untracked (new) files only.
- this.ValidateGitCommand("reset --mixed 95de01c9d5fb413b65d8ba097ed5aecf35477515");
- this.ValidateGitCommand("reset --mixed da8aaf4cd4d13677b2a71d04d4f3c7290a8ea0da");
+ this.ValidateGitCommand("reset --mixed c272d4846f2250edfb35fcac60b4b66bb17478fa");
+ this.ValidateGitCommand("reset --mixed 4275906774e9cc37a6875448cd3fcdc5b3ea2be3");
this.ValidateGitCommand("checkout " + this.ControlGitRepo.Commitish);
}
@@ -701,9 +701,9 @@ public void DeleteFolderAndChangeBranchToFolderWithDifferentCase()
this.FolderShouldHaveCaseMatchingName(folderName, "GVFlt_MultiThreadTest");
this.DeleteFolder(folderName);
- // b5fd7d23706a18cff3e2b8225588d479f7e51138 is the commit prior to deleting GVFLT_MultiThreadTest
+ // 4141dc6023b853740795db41a06b278ebdee0192 is the commit prior to deleting GVFLT_MultiThreadTest
// and re-adding it as as GVFlt_MultiThreadTest
- this.ValidateGitCommand("checkout b5fd7d23706a18cff3e2b8225588d479f7e51138");
+ this.ValidateGitCommand("checkout 4141dc6023b853740795db41a06b278ebdee0192");
this.FolderShouldHaveCaseMatchingName(folderName, "GVFLT_MultiThreadTest");
}
@@ -783,9 +783,9 @@ public void DeleteFileThenCheckout()
this.DeleteFile("GitCommandsTests\\DeleteFileTests\\1\\#test");
this.FolderShouldExistAndBeEmpty("GitCommandsTests\\DeleteFileTests\\1");
- // Commit 14cf226119766146b1fa5c5aa4cd0896d05f6b63 is before
+ // Commit cb2d05febf64e3b0df50bd8d3fe8f05c0e2caa47 is before
// the files in GitCommandsTests\DeleteFileTests were added
- this.ValidateGitCommand("checkout 14cf226119766146b1fa5c5aa4cd0896d05f6b63");
+ this.ValidateGitCommand("checkout cb2d05febf64e3b0df50bd8d3fe8f05c0e2caa47");
this.ShouldNotExistOnDisk("GitCommandsTests\\DeleteFileTests\\1");
this.ShouldNotExistOnDisk("GitCommandsTests\\DeleteFileTests");
@@ -799,7 +799,7 @@ public void CheckoutEditCheckoutWithoutFolderThenCheckoutWithMultipleFiles()
this.RunGitCommand("reset --hard -q HEAD");
// This commit should remove the DeleteFileWithNameAheadOfDotAndSwitchCommits folder
- this.ValidateGitCommand("checkout b4d932658def04a97da873fd6adab70014b8a523");
+ this.ValidateGitCommand("checkout 9ba05ac6706d3952995d0a54703fc724ddde57cc");
this.ShouldNotExistOnDisk("DeleteFileWithNameAheadOfDotAndSwitchCommits");
}
@@ -810,7 +810,7 @@ public void CreateAFolderThenCheckoutBranchWithFolder()
this.FolderShouldExistAndHaveFile("DeleteFileWithNameAheadOfDotAndSwitchCommits", "1");
// This commit should remove the DeleteFileWithNameAheadOfDotAndSwitchCommits folder
- this.ValidateGitCommand("checkout b4d932658def04a97da873fd6adab70014b8a523");
+ this.ValidateGitCommand("checkout 9ba05ac6706d3952995d0a54703fc724ddde57cc");
this.ShouldNotExistOnDisk("DeleteFileWithNameAheadOfDotAndSwitchCommits");
this.CreateFolder("DeleteFileWithNameAheadOfDotAndSwitchCommits");
this.ValidateGitCommand("checkout " + this.ControlGitRepo.Commitish);
diff --git a/GVFS/GVFS.FunctionalTests/Tests/GitCommands/StatusTests.cs b/GVFS/GVFS.FunctionalTests/Tests/GitCommands/StatusTests.cs
index 32af499e5e..a66d691c9f 100644
--- a/GVFS/GVFS.FunctionalTests/Tests/GitCommands/StatusTests.cs
+++ b/GVFS/GVFS.FunctionalTests/Tests/GitCommands/StatusTests.cs
@@ -1,5 +1,7 @@
-using NUnit.Framework;
+using GVFS.Tests.Should;
+using NUnit.Framework;
using System.IO;
+using System.Threading;
namespace GVFS.FunctionalTests.Tests.GitCommands
{
@@ -20,5 +22,117 @@ public void MoveFileIntoDotGitDirectory()
this.MoveFile(srcPath, dstPath);
this.ValidateGitCommand("status");
}
+
+ [TestCase]
+ public void ModifyingAndDeletingRepositoryExcludeFileInvalidatesCache()
+ {
+ string repositoryExcludeFile = Path.Combine(".git", "info", "exclude");
+
+ this.RepositoryIgnoreTestSetup();
+
+ // Add ignore pattern to existing exclude file
+ this.EditFile(repositoryExcludeFile, "*.ign");
+
+ // The exclude file has been modified, verify this status
+ // excludes the "test.ign" file as expected.
+ this.ValidateGitCommand("status");
+
+ // Wait for status cache
+ this.WaitForStatusCacheToBeGenerated();
+
+ // Delete repository exclude file
+ this.DeleteFile(repositoryExcludeFile);
+
+ // The exclude file has been deleted, verify this status
+ // includes the "test.ign" file as expected.
+ this.ValidateGitCommand("status");
+ }
+
+ [TestCase]
+ public void NewRepositoryExcludeFileInvalidatesCache()
+ {
+ string repositoryExcludeFileRelativePath = Path.Combine(".git", "info", "exclude");
+ string repositoryExcludeFilePath = Path.Combine(this.Enlistment.EnlistmentRoot, repositoryExcludeFileRelativePath);
+
+ this.DeleteFile(repositoryExcludeFileRelativePath);
+
+ this.RepositoryIgnoreTestSetup();
+
+ File.Exists(repositoryExcludeFilePath).ShouldBeFalse("Repository exclude path should not exist");
+
+ // Create new exclude file with ignore pattern
+ this.CreateFile(repositoryExcludeFileRelativePath, "*.ign");
+
+ // The exclude file has been modified, verify this status
+ // excludes the "test.ign" file as expected.
+ this.ValidateGitCommand("status");
+ }
+
+ [TestCase]
+ public void ModifyingHeadSymbolicRefInvalidatesCache()
+ {
+ this.ValidateGitCommand("status");
+
+ this.WaitForStatusCacheToBeGenerated(waitForNewFile: false);
+
+ this.ValidateGitCommand("branch other_branch");
+
+ this.WaitForStatusCacheToBeGenerated();
+ this.ValidateGitCommand("status");
+
+ this.ValidateGitCommand("symbolic-ref HEAD refs/heads/other_branch");
+ }
+
+ [TestCase]
+ public void ModifyingHeadRefInvalidatesCache()
+ {
+ this.ValidateGitCommand("status");
+
+ this.WaitForStatusCacheToBeGenerated(waitForNewFile: false);
+
+ this.ValidateGitCommand("update-ref HEAD HEAD~1");
+
+ this.WaitForStatusCacheToBeGenerated();
+ this.ValidateGitCommand("status");
+ }
+
+ private void RepositoryIgnoreTestSetup()
+ {
+ string statusCachePath = Path.Combine(this.Enlistment.DotGVFSRoot, "GitStatusCache", "GitStatusCache.dat");
+ File.Delete(statusCachePath);
+
+ // Create a new file with an extension that will be ignored later in the test.
+ this.CreateFile("test.ign", "file to be ignored");
+
+ this.WaitForStatusCacheToBeGenerated();
+
+ // Verify that status from the status cache includes the "test.ign" entry
+ this.ValidateGitCommand("status");
+ }
+
+ private void WaitForStatusCacheToBeGenerated(bool waitForNewFile = true)
+ {
+ string statusCachePath = Path.Combine(this.Enlistment.DotGVFSRoot, "GitStatusCache", "GitStatusCache.dat");
+
+ if (waitForNewFile)
+ {
+ File.Exists(statusCachePath).ShouldEqual(false, "Status cache file should not exist at this point - it should have been deleted by previous status command.");
+ }
+
+ // Wait for the status cache file to be regenerated
+ for (int i = 0; i < 10; i++)
+ {
+ if (File.Exists(statusCachePath))
+ {
+ break;
+ }
+
+ Thread.Sleep(1000);
+ }
+
+ // The cache file should exist by now. We want the next status to come from the
+ // cache and include the "test.ign" entry.
+ File.Exists(statusCachePath).ShouldEqual(true, "Status cache file should be regenerated by this point.");
+ }
}
}
diff --git a/GVFS/GVFS.FunctionalTests/Tests/MultiEnlistmentTests/SharedCacheTests.cs b/GVFS/GVFS.FunctionalTests/Tests/MultiEnlistmentTests/SharedCacheTests.cs
index b930b0836c..eb120e9207 100644
--- a/GVFS/GVFS.FunctionalTests/Tests/MultiEnlistmentTests/SharedCacheTests.cs
+++ b/GVFS/GVFS.FunctionalTests/Tests/MultiEnlistmentTests/SharedCacheTests.cs
@@ -20,7 +20,7 @@ public class SharedCacheTests : TestsWithMultiEnlistment
// This branch and commit sha should point to the same place.
private const string WellKnownBranch = "FunctionalTests/20170602";
- private const string WellKnownCommitSha = "b407df4e21261e2bf022ef7031fabcf21ee0e14d";
+ private const string WellKnownCommitSha = "79dc4233df4d9a7e053662bff95df498f640022e";
private string localCachePath;
private string localCacheParentPath;
diff --git a/GVFS/GVFS.FunctionalTests/Tools/GVFSFunctionalTestEnlistment.cs b/GVFS/GVFS.FunctionalTests/Tools/GVFSFunctionalTestEnlistment.cs
index 5daba90a92..1326cfd3be 100644
--- a/GVFS/GVFS.FunctionalTests/Tools/GVFSFunctionalTestEnlistment.cs
+++ b/GVFS/GVFS.FunctionalTests/Tools/GVFSFunctionalTestEnlistment.cs
@@ -6,7 +6,7 @@
using System.Collections.Generic;
using System.IO;
using System.Linq;
-using System.Runtime.InteropServices;
+using System.Runtime.InteropServices;
using System.Threading;
namespace GVFS.FunctionalTests.Tools
@@ -37,7 +37,7 @@ private GVFSFunctionalTestEnlistment(string pathToGVFS, string enlistmentRoot, s
{
// eg C:\Repos\GVFSFunctionalTests\.gvfsCache
// Ensures the general cache is not cleaned up between test runs
- localCacheRoot = Path.Combine(Properties.Settings.Default.EnlistmentRoot, "..", ".gvfsCache");
+ localCacheRoot = Path.Combine(Properties.Settings.Default.EnlistmentRoot, "..", ".gvfsCache");
}
}
@@ -152,6 +152,18 @@ public void CloneAndMount()
GitProcess.Invoke(this.RepoRoot, "config core.abbrev 40");
GitProcess.Invoke(this.RepoRoot, "config user.name \"Functional Test User\"");
GitProcess.Invoke(this.RepoRoot, "config user.email \"functional@test.com\"");
+
+ // If this repository has a .gitignore file in the root directory, force it to be
+ // hydrated. This is because if the GitStatusCache feature is enabled, it will run
+ // a "git status" command asynchronously, which will hydrate the .gitignore file
+ // as it reads the ignore rules. Hydrate this file here so that it is consistently
+ // hydrated and there are no race conditions depending on when / if it is hydrated
+ // as part of an asynchronous status scan to rebuild the GitStatusCache.
+ string rootGitIgnorePath = Path.Combine(this.RepoRoot, ".gitignore");
+ if (File.Exists(rootGitIgnorePath))
+ {
+ File.ReadAllBytes(rootGitIgnorePath);
+ }
}
public void MountGVFS()
diff --git a/GVFS/GVFS.Installer/Setup.iss b/GVFS/GVFS.Installer/Setup.iss
index 9c9772aa6a..36bff4d3cc 100644
--- a/GVFS/GVFS.Installer/Setup.iss
+++ b/GVFS/GVFS.Installer/Setup.iss
@@ -4,7 +4,7 @@
; General documentation on how to use InnoSetup scripts: http://www.jrsoftware.org/ishelp/index.php
#define PrjFltDir PackagesDir + "\" + ProjFSPackage + "\filter"
-#define VCRuntimeDir PackagesDir + "\GVFS.VCRuntime.0.1.0-build\lib\x64"
+#define VCRuntimeDir PackagesDir + "\GVFS.VCRuntime.0.2.0-build\lib\x64"
#define GVFSDir BuildOutputDir + "\GVFS.Windows\bin\" + PlatformAndConfiguration
#define GVFSCommonDir BuildOutputDir + "\GVFS.Common\bin\" + PlatformAndConfiguration + "\netstandard2.0"
#define HooksDir BuildOutputDir + "\GVFS.Hooks.Windows\bin\" + PlatformAndConfiguration
@@ -63,6 +63,10 @@ Name: "full"; Description: "Full installation"; Flags: iscustom;
[Components]
+[InstallDelete]
+; Delete old dependencies from VS 2015 VC redistributables
+Type: files; Name: "{app}\ucrtbase.dll"
+
[Files]
; PrjFlt Filter Files
DestDir: "{app}\Filter"; Flags: ignoreversion; Source:"{#PrjFltDir}\PrjFlt.sys"
@@ -97,9 +101,10 @@ DestDir: "{app}"; Flags: ignoreversion; Source:"{#ReadObjectDir}\GVFS.ReadObject
DestDir: "{app}"; Flags: ignoreversion; Source:"{#VirtualFileSystemDir}\GVFS.VirtualFileSystemHook.pdb"
DestDir: "{app}"; Flags: ignoreversion; Source:"{#VirtualFileSystemDir}\GVFS.VirtualFileSystemHook.exe"
-; Cpp Dependancies
-DestDir: "{app}"; Flags: ignoreversion; Source:"{#VCRuntimeDir}\ucrtbase.dll"
+; Cpp Dependencies
DestDir: "{app}"; Flags: ignoreversion; Source:"{#VCRuntimeDir}\msvcp140.dll"
+DestDir: "{app}"; Flags: ignoreversion; Source:"{#VCRuntimeDir}\msvcp140_1.dll"
+DestDir: "{app}"; Flags: ignoreversion; Source:"{#VCRuntimeDir}\msvcp140_2.dll"
DestDir: "{app}"; Flags: ignoreversion; Source:"{#VCRuntimeDir}\vcruntime140.dll"
; GVFS PDB's
@@ -278,6 +283,18 @@ begin
end;
end;
+procedure WriteGitStatusCacheAvailableFile();
+var
+ TokenFilePath: string;
+begin
+ TokenFilePath := ExpandConstant('{app}\GitStatusCacheAvailable');
+ if not FileExists(TokenFilePath) then
+ begin
+ Log('WritingGitStatusCacheAvailableFile: Writing file ' + TokenFilePath);
+ SaveStringToFile(TokenFilePath, '', False);
+ end
+end;
+
procedure InstallGVFSService();
var
ResultCode: integer;
@@ -302,6 +319,7 @@ begin
end;
end;
+ WriteGitStatusCacheAvailableFile();
finally
WizardForm.StatusLabel.Caption := StatusText;
WizardForm.ProgressGauge.Style := npbstNormal;
diff --git a/GVFS/GVFS.Mount/InProcessMount.cs b/GVFS/GVFS.Mount/InProcessMount.cs
index 6d6039762d..6fe957eab5 100644
--- a/GVFS/GVFS.Mount/InProcessMount.cs
+++ b/GVFS/GVFS.Mount/InProcessMount.cs
@@ -1,4 +1,4 @@
-using GVFS.Common;
+using GVFS.Common;
using GVFS.Common.FileSystem;
using GVFS.Common.Git;
using GVFS.Common.Http;
@@ -33,6 +33,7 @@ public class InProcessMount
private CacheServerInfo cacheServer;
private RetryConfig retryConfig;
+ private GitStatusCacheConfig gitStatusCacheConfig;
private GVFSContext context;
private GVFSGitObjects gitObjects;
@@ -41,10 +42,11 @@ public class InProcessMount
private HeartbeatThread heartbeat;
private ManualResetEvent unmountEvent;
- public InProcessMount(ITracer tracer, GVFSEnlistment enlistment, CacheServerInfo cacheServer, RetryConfig retryConfig, bool showDebugWindow)
+ public InProcessMount(ITracer tracer, GVFSEnlistment enlistment, CacheServerInfo cacheServer, RetryConfig retryConfig, GitStatusCacheConfig gitStatusCacheConfig, bool showDebugWindow)
{
this.tracer = tracer;
this.retryConfig = retryConfig;
+ this.gitStatusCacheConfig = gitStatusCacheConfig;
this.cacheServer = cacheServer;
this.enlistment = enlistment;
this.showDebugWindow = showDebugWindow;
@@ -290,18 +292,17 @@ private void HandleLockRequest(string messageBody, NamedPipeServer.Connection co
bool lockAcquired = false;
NamedPipeMessages.LockData existingExternalHolder = null;
+ string denyGVFSMessage = null;
+
bool lockAvailable = this.context.Repository.GVFSLock.IsLockAvailableForExternalRequestor(out existingExternalHolder);
+ bool isReadyForExternalLockRequests = this.fileSystemCallbacks.IsReadyForExternalAcquireLockRequests(requester, out denyGVFSMessage);
- string denyGVFSMessage = null;
- if (!requester.CheckAvailabilityOnly)
+ if (!requester.CheckAvailabilityOnly && isReadyForExternalLockRequests)
{
- if (this.fileSystemCallbacks.IsReadyForExternalAcquireLockRequests(requester, out denyGVFSMessage))
- {
- lockAcquired = this.context.Repository.GVFSLock.TryAcquireLockForExternalRequestor(requester, out existingExternalHolder);
- }
+ lockAcquired = this.context.Repository.GVFSLock.TryAcquireLockForExternalRequestor(requester, out existingExternalHolder);
}
- if (lockAvailable && requester.CheckAvailabilityOnly)
+ if (requester.CheckAvailabilityOnly && lockAvailable && isReadyForExternalLockRequests)
{
response = new NamedPipeMessages.AcquireLock.Response(NamedPipeMessages.AcquireLock.AvailableResult);
}
@@ -506,7 +507,14 @@ private void MountAndStartWorkingDirectoryCallbacks(CacheServerInfo cache)
GitObjectsHttpRequestor objectRequestor = new GitObjectsHttpRequestor(this.context.Tracer, this.context.Enlistment, cache, this.retryConfig);
this.gitObjects = new GVFSGitObjects(this.context, objectRequestor);
FileSystemVirtualizer virtualizer = this.CreateOrReportAndExit(() => GVFSPlatformLoader.CreateFileSystemVirtualizer(this.context, this.gitObjects), "Failed to create src folder virtualizer");
- this.fileSystemCallbacks = this.CreateOrReportAndExit(() => new FileSystemCallbacks(this.context, this.gitObjects, RepoMetadata.Instance, virtualizer), "Failed to create src folder callback listener");
+
+ GitStatusCache gitStatusCache = (!this.context.Unattended && GVFSPlatform.Instance.IsGitStatusCacheSupported()) ? new GitStatusCache(this.context, this.gitStatusCacheConfig) : null;
+ if (gitStatusCache != null)
+ {
+ this.tracer.RelatedInfo("Git status cache enabled. Backoff time: {0}ms", this.gitStatusCacheConfig.BackoffTime.TotalMilliseconds);
+ }
+
+ this.fileSystemCallbacks = this.CreateOrReportAndExit(() => new FileSystemCallbacks(this.context, this.gitObjects, RepoMetadata.Instance, virtualizer, gitStatusCache), "Failed to create src folder callback listener");
if (!this.context.Unattended)
{
diff --git a/GVFS/GVFS.Mount/InProcessMountVerb.cs b/GVFS/GVFS.Mount/InProcessMountVerb.cs
index 8ab43705d2..b1c1e19ce3 100644
--- a/GVFS/GVFS.Mount/InProcessMountVerb.cs
+++ b/GVFS/GVFS.Mount/InProcessMountVerb.cs
@@ -93,7 +93,14 @@ public void Execute()
this.ReportErrorAndExit(tracer, "Failed to determine GVFS timeout and max retries: " + error);
}
- InProcessMount mountHelper = new InProcessMount(tracer, enlistment, cacheServer, retryConfig, this.ShowDebugWindow);
+ GitStatusCacheConfig gitStatusCacheConfig;
+ if (!GitStatusCacheConfig.TryLoadFromGitConfig(tracer, enlistment, out gitStatusCacheConfig, out error))
+ {
+ tracer.RelatedWarning("Failed to determine GVFS status cache backoff time: " + error);
+ gitStatusCacheConfig = GitStatusCacheConfig.DefaultConfig;
+ }
+
+ InProcessMount mountHelper = new InProcessMount(tracer, enlistment, cacheServer, retryConfig, gitStatusCacheConfig, this.ShowDebugWindow);
try
{
diff --git a/GVFS/GVFS.NativeTests/GVFS.NativeTests.vcxproj b/GVFS/GVFS.NativeTests/GVFS.NativeTests.vcxproj
index df90f548b0..5bd2694e11 100644
--- a/GVFS/GVFS.NativeTests/GVFS.NativeTests.vcxproj
+++ b/GVFS/GVFS.NativeTests/GVFS.NativeTests.vcxproj
@@ -22,13 +22,13 @@
DynamicLibrary
true
- v140
+ v141
NotSet
DynamicLibrary
false
- v140
+ v141
true
NotSet
@@ -154,4 +154,4 @@
-
\ No newline at end of file
+
diff --git a/GVFS/GVFS.PerfProfiling/ProfilingEnvironment.cs b/GVFS/GVFS.PerfProfiling/ProfilingEnvironment.cs
index 4e00bf7f68..8f4d6a777f 100644
--- a/GVFS/GVFS.PerfProfiling/ProfilingEnvironment.cs
+++ b/GVFS/GVFS.PerfProfiling/ProfilingEnvironment.cs
@@ -78,7 +78,7 @@ private FileSystemCallbacks CreateFileSystemCallbacks()
new RetryConfig());
GVFSGitObjects gitObjects = new GVFSGitObjects(this.Context, objectRequestor);
- return new FileSystemCallbacks(this.Context, gitObjects, RepoMetadata.Instance, fileSystemVirtualizer: null);
+ return new FileSystemCallbacks(this.Context, gitObjects, RepoMetadata.Instance, fileSystemVirtualizer: null, gitStatusCache : null);
}
}
}
diff --git a/GVFS/GVFS.Platform.Mac/MacPlatform.cs b/GVFS/GVFS.Platform.Mac/MacPlatform.cs
index a621a85bbd..d11b9d5bd9 100644
--- a/GVFS/GVFS.Platform.Mac/MacPlatform.cs
+++ b/GVFS/GVFS.Platform.Mac/MacPlatform.cs
@@ -121,5 +121,11 @@ public override bool TryGetGVFSEnlistmentRoot(string directory, out string enlis
{
return MacPlatform.TryGetGVFSEnlistmentRootImplementation(directory, out enlistmentRoot, out errorMessage);
}
+
+ public override bool IsGitStatusCacheSupported()
+ {
+ // TODO(Mac): support git status cache
+ return false;
+ }
}
}
diff --git a/GVFS/GVFS.Platform.Windows/ActiveEnumeration.cs b/GVFS/GVFS.Platform.Windows/ActiveEnumeration.cs
index f0b7236d42..6caa10785d 100644
--- a/GVFS/GVFS.Platform.Windows/ActiveEnumeration.cs
+++ b/GVFS/GVFS.Platform.Windows/ActiveEnumeration.cs
@@ -1,20 +1,21 @@
using GVFS.Virtualization.Projection;
-using System;
+
using System.Collections.Generic;
namespace GVFS.Platform.Windows
{
- public class ActiveEnumeration : IDisposable
+ public class ActiveEnumeration
{
private static FileNamePatternMatcher doesPatternMatch = null;
- private readonly IEnumerable fileInfos;
- private IEnumerator fileInfoEnumerator;
+ // Use our own enumerator to avoid having to dispose anything
+ private ProjectedFileInfoEnumerator fileInfoEnumerator;
+
private string filterString = null;
- public ActiveEnumeration(IEnumerable fileInfos)
+ public ActiveEnumeration(List fileInfos)
{
- this.fileInfos = fileInfos;
+ this.fileInfoEnumerator = new ProjectedFileInfoEnumerator(fileInfos);
this.ResetEnumerator();
this.MoveNext();
}
@@ -106,15 +107,6 @@ public string GetFilterString()
return this.filterString;
}
- public void Dispose()
- {
- if (this.fileInfoEnumerator != null)
- {
- this.fileInfoEnumerator.Dispose();
- this.fileInfoEnumerator = null;
- }
- }
-
private void SaveFilter(string filter)
{
if (string.IsNullOrEmpty(filter))
@@ -138,7 +130,44 @@ private bool IsCurrentHidden()
private void ResetEnumerator()
{
- this.fileInfoEnumerator = this.fileInfos.GetEnumerator();
+ this.fileInfoEnumerator.Reset();
+ }
+
+ private class ProjectedFileInfoEnumerator
+ {
+ private List list;
+ private int index;
+
+ public ProjectedFileInfoEnumerator(List projectedFileInfos)
+ {
+ this.list = projectedFileInfos;
+ this.Reset();
+ }
+
+ public ProjectedFileInfo Current { get; private set; }
+
+ // Combination of the logic in List.Enumerator MoveNext() and MoveNextRare()
+ // https://github.com/dotnet/corefx/blob/b492409b4a1952cda4b078f800499d382e1765fc/src/Common/src/CoreLib/System/Collections/Generic/List.cs#L1137
+ // (No need to check list._version as GVFS does not modify the lists used for enumeration)
+ public bool MoveNext()
+ {
+ if (this.index < this.list.Count)
+ {
+ this.Current = this.list[this.index];
+ this.index++;
+ return true;
+ }
+
+ this.index = this.list.Count + 1;
+ this.Current = null;
+ return false;
+ }
+
+ public void Reset()
+ {
+ this.index = 0;
+ this.Current = null;
+ }
}
}
}
diff --git a/GVFS/GVFS.Platform.Windows/DiskLayoutUpgrades/DiskLayout15to16Upgrade_GitStatusCache.cs b/GVFS/GVFS.Platform.Windows/DiskLayoutUpgrades/DiskLayout15to16Upgrade_GitStatusCache.cs
new file mode 100644
index 0000000000..db1557e000
--- /dev/null
+++ b/GVFS/GVFS.Platform.Windows/DiskLayoutUpgrades/DiskLayout15to16Upgrade_GitStatusCache.cs
@@ -0,0 +1,30 @@
+using GVFS.Common.Tracing;
+using GVFS.DiskLayoutUpgrades;
+
+namespace GVFS.Platform.Windows.DiskLayoutUpgrades
+{
+ ///
+ /// This is a no-op upgrade step. It is here to prevent users from downgrading to a previous
+ /// version of GVFS that is not GitStatusCache aware.
+ ///
+ /// This is because GVFS will set git config entries for the location of the git status cache when mounting,
+ /// but does not unset them when unmounting (even if it did, it might not reliably unset these values).
+ /// If a user downgrades, and they have a status cache file on disk, and git is configured to use the cache,
+ /// then they might get stale / incorrect results after a downgrade. To avoid this possibility, we update
+ /// the on-disk version during upgrade.
+ ///
+ public class DiskLayout15to16Upgrade_GitStatusCache : DiskLayoutUpgrade.MajorUpgrade
+ {
+ protected override int SourceMajorVersion => 15;
+
+ public override bool TryUpgrade(ITracer tracer, string enlistmentRoot)
+ {
+ if (!this.TryIncrementMajorVersion(tracer, enlistmentRoot))
+ {
+ return false;
+ }
+
+ return true;
+ }
+ }
+}
diff --git a/GVFS/GVFS.Platform.Windows/DiskLayoutUpgrades/WindowsDiskLayoutUpgradeData.cs b/GVFS/GVFS.Platform.Windows/DiskLayoutUpgrades/WindowsDiskLayoutUpgradeData.cs
index 8b5caf1851..36b6bafc43 100644
--- a/GVFS/GVFS.Platform.Windows/DiskLayoutUpgrades/WindowsDiskLayoutUpgradeData.cs
+++ b/GVFS/GVFS.Platform.Windows/DiskLayoutUpgrades/WindowsDiskLayoutUpgradeData.cs
@@ -26,6 +26,7 @@ public DiskLayoutUpgrade[] Upgrades
new DiskLayout12to13Upgrade_FolderPlaceholder(),
new DiskLayout13to14Upgrade_BlobSizes(),
new DiskLayout14to15Upgrade_ModifiedPaths(),
+ new DiskLayout15to16Upgrade_GitStatusCache(),
};
}
}
diff --git a/GVFS/GVFS.Platform.Windows/GVFS.Platform.Windows.csproj b/GVFS/GVFS.Platform.Windows/GVFS.Platform.Windows.csproj
index 226683c05a..7a86d6226d 100644
--- a/GVFS/GVFS.Platform.Windows/GVFS.Platform.Windows.csproj
+++ b/GVFS/GVFS.Platform.Windows/GVFS.Platform.Windows.csproj
@@ -78,6 +78,7 @@
+
diff --git a/GVFS/GVFS.Platform.Windows/WindowsFileSystemVirtualizer.cs b/GVFS/GVFS.Platform.Windows/WindowsFileSystemVirtualizer.cs
index f7814c8e2b..4f3b19b4e7 100644
--- a/GVFS/GVFS.Platform.Windows/WindowsFileSystemVirtualizer.cs
+++ b/GVFS/GVFS.Platform.Windows/WindowsFileSystemVirtualizer.cs
@@ -174,6 +174,9 @@ protected override bool TryStart(out string error)
new NotificationMapping(NotificationType.None, GVFSConstants.DotGit.Root),
new NotificationMapping(Notifications.IndexFile, GVFSConstants.DotGit.Index),
new NotificationMapping(Notifications.LogsHeadFile, GVFSConstants.DotGit.Logs.Head),
+ new NotificationMapping(Notifications.ExcludeAndHeadFile, GVFSConstants.DotGit.Info.ExcludePath),
+ new NotificationMapping(Notifications.ExcludeAndHeadFile, GVFSConstants.DotGit.Head),
+ new NotificationMapping(Notifications.FilesAndFoldersInRefsHeads, GVFSConstants.DotGit.Refs.Heads.Root),
};
// We currently use twice as many threads as connections to allow for
@@ -277,7 +280,7 @@ private HResult StartDirectoryEnumerationHandler(int commandId, Guid enumeration
return (HResult)HResultExtensions.HResultFromNtStatus.DeviceNotReady;
}
- IEnumerable projectedItems;
+ List projectedItems;
if (this.FileSystemCallbacks.GitIndexProjection.TryGetProjectedItemsFromMemory(virtualPath, out projectedItems))
{
ActiveEnumeration activeEnumeration = new ActiveEnumeration(projectedItems);
@@ -287,7 +290,6 @@ private HResult StartDirectoryEnumerationHandler(int commandId, Guid enumeration
this.CreateEventMetadata(enumerationId, virtualPath),
nameof(this.StartDirectoryEnumerationHandler) + ": Failed to add enumeration ID to active collection");
- activeEnumeration.Dispose();
return HResult.InternalError;
}
@@ -357,7 +359,6 @@ private void StartDirectoryEnumerationAsyncHandler(
this.CreateEventMetadata(enumerationId, virtualPath),
nameof(this.StartDirectoryEnumerationAsyncHandler) + ": Failed to add enumeration ID to active collection");
- activeEnumeration.Dispose();
result = HResult.InternalError;
}
else
@@ -406,11 +407,6 @@ private void StartDirectoryEnumerationAsyncHandler(
ActiveEnumeration activeEnumeration;
bool activeEnumerationsUpdated = this.activeEnumerations.TryRemove(enumerationId, out activeEnumeration);
- if (activeEnumerationsUpdated)
- {
- activeEnumeration.Dispose();
- }
-
metadata.Add("activeEnumerationsUpdated", activeEnumerationsUpdated);
this.Context.Tracer.RelatedEvent(EventLevel.Informational, $"{nameof(this.StartDirectoryEnumerationAsyncHandler)}_CommandAlreadyCanceled", metadata);
}
@@ -421,11 +417,7 @@ private HResult EndDirectoryEnumerationHandler(Guid enumerationId)
try
{
ActiveEnumeration activeEnumeration;
- if (this.activeEnumerations.TryRemove(enumerationId, out activeEnumeration))
- {
- activeEnumeration.Dispose();
- }
- else
+ if (!this.activeEnumerations.TryRemove(enumerationId, out activeEnumeration))
{
this.Context.Tracer.RelatedWarning(
this.CreateEventMetadata(enumerationId),
@@ -1238,7 +1230,7 @@ private void NotifyFileRenamedHandler(
if (dstPathInDotGit)
{
- this.OnDotGitFileChanged(destinationPath);
+ this.OnDotGitFileOrFolderChanged(destinationPath);
}
if (!(srcPathInDotGit && dstPathInDotGit))
@@ -1272,25 +1264,41 @@ private void NotifyFileHandleClosedFileModifiedOrDeletedHandler(
{
bool pathInsideDotGit = FileSystemCallbacks.IsPathInsideDotGit(virtualPath);
- if (isFileModified && pathInsideDotGit)
+ if (isFileModified)
{
- // TODO 876861: See if ProjFS can provide process ID\name in this callback
- this.OnDotGitFileChanged(virtualPath);
+ if (pathInsideDotGit)
+ {
+ // TODO 876861: See if ProjFS can provide process ID\name in this callback
+ this.OnDotGitFileOrFolderChanged(virtualPath);
+ }
+ else
+ {
+ this.FileSystemCallbacks.InvalidateGitStatusCache();
+ }
}
- else if (isFileDeleted && !pathInsideDotGit)
+ else if (isFileDeleted)
{
- if (isDirectory)
+ if (pathInsideDotGit)
{
- // Don't want to add folders to the modified list if git is the one deleting the directory
- GitCommandLineParser gitCommand = new GitCommandLineParser(this.Context.Repository.GVFSLock.GetLockedGitCommand());
- if (!gitCommand.IsValidGitCommand)
- {
- this.FileSystemCallbacks.OnFolderDeleted(virtualPath);
- }
+ this.OnDotGitFileOrFolderDeleted(virtualPath);
}
else
{
- this.FileSystemCallbacks.OnFileDeleted(virtualPath);
+ if (isDirectory)
+ {
+ // Don't want to add folders to the modified list if git is the one deleting the directory
+ GitCommandLineParser gitCommand = new GitCommandLineParser(this.Context.Repository.GVFSLock.GetLockedGitCommand());
+ if (!gitCommand.IsValidGitCommand)
+ {
+ this.FileSystemCallbacks.OnFolderDeleted(virtualPath);
+ }
+ }
+ else
+ {
+ this.FileSystemCallbacks.OnFileDeleted(virtualPath);
+ }
+
+ this.FileSystemCallbacks.InvalidateGitStatusCache();
}
}
}
@@ -1376,7 +1384,7 @@ private class GetFileStreamException : Exception
{
public GetFileStreamException(HResult errorCode)
: this("GetFileStreamException exception, error: " + errorCode.ToString(), errorCode)
- {
+ {
}
public GetFileStreamException(string message, HResult result)
@@ -1398,17 +1406,28 @@ private class Notifications
NotificationType.FileRenamed |
NotificationType.FileHandleClosedFileModified;
+ public const NotificationType ExcludeAndHeadFile =
+ NotificationType.FileRenamed |
+ NotificationType.FileHandleClosedFileDeleted |
+ NotificationType.FileHandleClosedFileModified;
+
+ public const NotificationType FilesAndFoldersInRefsHeads =
+ NotificationType.FileRenamed |
+ NotificationType.FileHandleClosedFileDeleted |
+ NotificationType.FileHandleClosedFileModified;
+
public const NotificationType FilesInWorkingFolder =
NotificationType.NewFileCreated |
NotificationType.FileSupersededOrOverwritten |
NotificationType.FileRenamed |
NotificationType.FileHandleClosedFileDeleted |
- NotificationType.FilePreConvertToFull;
+ NotificationType.FilePreConvertToFull |
+ NotificationType.FileHandleClosedFileModified;
public const NotificationType FoldersInWorkingFolder =
NotificationType.NewFileCreated |
NotificationType.FileRenamed |
NotificationType.FileHandleClosedFileDeleted;
- }
+ }
}
}
diff --git a/GVFS/GVFS.Platform.Windows/WindowsPlatform.cs b/GVFS/GVFS.Platform.Windows/WindowsPlatform.cs
index 4100e80318..c7c0b6018f 100644
--- a/GVFS/GVFS.Platform.Windows/WindowsPlatform.cs
+++ b/GVFS/GVFS.Platform.Windows/WindowsPlatform.cs
@@ -248,6 +248,11 @@ public override bool IsConsoleOutputRedirectedToFile()
return WindowsPlatform.IsConsoleOutputRedirectedToFileImplementation();
}
+ public override bool IsGitStatusCacheSupported()
+ {
+ return File.Exists(Path.Combine(Paths.GetServiceDataRoot(GVFSConstants.Service.ServiceName), GVFSConstants.GitStatusCache.EnableGitStatusCacheTokenFile));
+ }
+
public override bool TryGetGVFSEnlistmentRoot(string directory, out string enlistmentRoot, out string errorMessage)
{
return WindowsPlatform.TryGetGVFSEnlistmentRootImplementation(directory, out enlistmentRoot, out errorMessage);
diff --git a/GVFS/GVFS.ReadObjectHook/GVFS.ReadObjectHook.Windows.vcxproj b/GVFS/GVFS.ReadObjectHook/GVFS.ReadObjectHook.Windows.vcxproj
index 272e6bfb67..dd8a419948 100644
--- a/GVFS/GVFS.ReadObjectHook/GVFS.ReadObjectHook.Windows.vcxproj
+++ b/GVFS/GVFS.ReadObjectHook/GVFS.ReadObjectHook.Windows.vcxproj
@@ -23,13 +23,13 @@
Application
true
- v140
+ v141
MultiByte
Application
false
- v140
+ v141
true
MultiByte
diff --git a/GVFS/GVFS.Service/GvfsService.cs b/GVFS/GVFS.Service/GvfsService.cs
index 4e6fdbcc07..bacab04575 100644
--- a/GVFS/GVFS.Service/GvfsService.cs
+++ b/GVFS/GVFS.Service/GvfsService.cs
@@ -42,6 +42,8 @@ public void Run()
using (NamedPipeServer pipeServer = NamedPipeServer.StartNewServer(pipeName, this.tracer, this.HandleRequest))
{
+ this.CheckEnableGitStatusCacheTokenFile();
+
using (ITracer activity = this.tracer.StartActivity("EnsurePrjFltHealthy", EventLevel.Informational))
{
string error;
@@ -273,6 +275,65 @@ private void HandleRequest(ITracer tracer, string request, NamedPipeServer.Conne
}
}
+ ///
+ /// To work around a behavior in ProjFS where notification masks on files that have been opened in virtualization instance are not invalidated
+ /// when the virtualization instance is restarted, GVFS waits until after there has been a reboot before enabling the GitStatusCache.
+ /// GVFS.Service signals that there has been a reboot since installing a version of GVFS that supports the GitStatusCache via
+ /// the existence of the file "EnableGitStatusCacheToken.dat" in {CommonApplicationData}\GVFS\GVFS.Service
+ /// (i.e. ProgramData\GVFS\GVFS.Service\EnableGitStatusCacheToken.dat on Windows).
+ ///
+ private void CheckEnableGitStatusCacheTokenFile()
+ {
+ try
+ {
+ string statusCacheVersionTokenPath = Path.Combine(Paths.GetServiceDataRoot(GVFSConstants.Service.ServiceName), GVFSConstants.GitStatusCache.EnableGitStatusCacheTokenFile);
+
+ if (!File.Exists(statusCacheVersionTokenPath))
+ {
+ DateTime lastRebootTime = NativeMethods.GetLastRebootTime();
+
+ // When a version of GVFS that supports the GitStatusCache is installed, it will create
+ // the following file. By checking the time the file was created, we know when that
+ // version of GVFS was installed.
+ string fileToCheck = Path.Combine(Configuration.AssemblyPath, "GitStatusCacheAvailable");
+ if (File.Exists(fileToCheck))
+ {
+ DateTime installTime = File.GetCreationTime(fileToCheck);
+ if (lastRebootTime > installTime)
+ {
+ File.WriteAllText(statusCacheVersionTokenPath, string.Empty);
+ }
+ }
+ else
+ {
+ this.tracer.RelatedError($"Unable to determine GVFS installation time: {fileToCheck} does not exist.");
+ }
+ }
+ }
+ catch (Exception ex)
+ {
+ // Do not crash the service if there is an error here. Service is still healthy, but we
+ // might not create file indicating that it is OK to use GitStatusCache.
+ this.tracer.RelatedError($"{nameof(CheckEnableGitStatusCacheTokenFile)}: Unable to determine GVFS installation time or write EnableGitStatusCacheToken file due to exception. Exception: {ex.ToString()}");
+ }
+ }
+
+ private bool TryGetGVFSInstallTime(out DateTime installTime)
+ {
+ installTime = DateTime.Now;
+
+ // Get the time of a file that was created by the GVFS installer (for a version of GVFS that supports the
+ // GitStatusCache). The expected path is written by the installer.
+ string fileToCheck = Path.Combine(Configuration.AssemblyPath, "GitStatusCacheAvailable");
+ if (File.Exists(fileToCheck))
+ {
+ installTime = File.GetCreationTime(fileToCheck);
+ return true;
+ }
+
+ return false;
+ }
+
private void LogExceptionAndExit(Exception e, string method)
{
EventMetadata metadata = new EventMetadata();
@@ -282,4 +343,4 @@ private void LogExceptionAndExit(Exception e, string method)
Environment.Exit((int)ReturnCode.GenericError);
}
}
-}
\ No newline at end of file
+}
diff --git a/GVFS/GVFS.UnitTests.Windows/Windows/Virtualization/ActiveEnumerationTests.cs b/GVFS/GVFS.UnitTests.Windows/Windows/Virtualization/ActiveEnumerationTests.cs
index 2496c1e32a..f74f42815c 100644
--- a/GVFS/GVFS.UnitTests.Windows/Windows/Virtualization/ActiveEnumerationTests.cs
+++ b/GVFS/GVFS.UnitTests.Windows/Windows/Virtualization/ActiveEnumerationTests.cs
@@ -34,13 +34,14 @@ public static object[] Runners
[TestCase]
public void EnumerationHandlesEmptyList()
{
- using (ActiveEnumeration activeEnumeration = new ActiveEnumeration(new List()))
- {
- activeEnumeration.IsCurrentValid.ShouldEqual(false);
- activeEnumeration.MoveNext().ShouldEqual(false);
- activeEnumeration.RestartEnumeration(string.Empty);
- activeEnumeration.IsCurrentValid.ShouldEqual(false);
- }
+ ActiveEnumeration activeEnumeration = CreateActiveEnumeration(new List());
+
+ activeEnumeration.MoveNext().ShouldEqual(false);
+ activeEnumeration.Current.ShouldEqual(null);
+
+ activeEnumeration.RestartEnumeration(string.Empty);
+ activeEnumeration.IsCurrentValid.ShouldEqual(false);
+ activeEnumeration.Current.ShouldEqual(null);
}
[TestCase]
@@ -51,10 +52,8 @@ public void EnumerateSingleEntryList()
new ProjectedFileInfo("a", size: 0, isFolder:false, sha: new Sha1Id(1, 1, 1))
};
- using (ActiveEnumeration activeEnumeration = new ActiveEnumeration(entries))
- {
- this.ValidateActiveEnumeratorReturnsAllEntries(activeEnumeration, entries);
- }
+ ActiveEnumeration activeEnumeration = CreateActiveEnumeration(entries);
+ this.ValidateActiveEnumeratorReturnsAllEntries(activeEnumeration, entries);
}
[TestCase]
@@ -70,10 +69,8 @@ public void EnumerateMultipleEntries()
new ProjectedFileInfo("E.bat", size: 0, isFolder:false, sha: new Sha1Id(1, 1, 5)),
};
- using (ActiveEnumeration activeEnumeration = new ActiveEnumeration(entries))
- {
- this.ValidateActiveEnumeratorReturnsAllEntries(activeEnumeration, entries);
- }
+ ActiveEnumeration activeEnumeration = CreateActiveEnumeration(entries);
+ this.ValidateActiveEnumeratorReturnsAllEntries(activeEnumeration, entries);
}
[TestCase]
@@ -85,18 +82,14 @@ public void EnumerateSingleEntryListWithEmptyFilter()
};
// Test empty string ("") filter
- using (ActiveEnumeration activeEnumeration = new ActiveEnumeration(entries))
- {
- activeEnumeration.TrySaveFilterString(string.Empty).ShouldEqual(true);
- this.ValidateActiveEnumeratorReturnsAllEntries(activeEnumeration, entries);
- }
+ ActiveEnumeration activeEnumeration = CreateActiveEnumeration(entries);
+ activeEnumeration.TrySaveFilterString(string.Empty).ShouldEqual(true);
+ this.ValidateActiveEnumeratorReturnsAllEntries(activeEnumeration, entries);
// Test null filter
- using (ActiveEnumeration activeEnumeration = new ActiveEnumeration(entries))
- {
- activeEnumeration.TrySaveFilterString(null).ShouldEqual(true);
- this.ValidateActiveEnumeratorReturnsAllEntries(activeEnumeration, entries);
- }
+ activeEnumeration = CreateActiveEnumeration(entries);
+ activeEnumeration.TrySaveFilterString(null).ShouldEqual(true);
+ this.ValidateActiveEnumeratorReturnsAllEntries(activeEnumeration, entries);
}
[TestCase]
@@ -107,29 +100,29 @@ public void EnumerateSingleEntryListWithWildcardFilter()
new ProjectedFileInfo("a", size: 0, isFolder:false, sha: new Sha1Id(1, 1, 1))
};
- using (ActiveEnumeration activeEnumeration = new ActiveEnumeration(entries))
- {
- activeEnumeration.TrySaveFilterString("*").ShouldEqual(true);
- this.ValidateActiveEnumeratorReturnsAllEntries(activeEnumeration, entries);
- }
+ ActiveEnumeration activeEnumeration = CreateActiveEnumeration(entries);
+ activeEnumeration.TrySaveFilterString("*").ShouldEqual(true);
+ this.ValidateActiveEnumeratorReturnsAllEntries(activeEnumeration, entries);
- using (ActiveEnumeration activeEnumeration = new ActiveEnumeration(entries))
- {
- activeEnumeration.TrySaveFilterString("?").ShouldEqual(true);
- this.ValidateActiveEnumeratorReturnsAllEntries(activeEnumeration, entries);
- }
+ activeEnumeration = CreateActiveEnumeration(entries);
+ activeEnumeration.TrySaveFilterString("?").ShouldEqual(true);
+ this.ValidateActiveEnumeratorReturnsAllEntries(activeEnumeration, entries);
- using (ActiveEnumeration activeEnumeration = new ActiveEnumeration(entries))
- {
- string filter = "*.*";
- activeEnumeration.TrySaveFilterString(filter).ShouldEqual(true);
+ activeEnumeration = CreateActiveEnumeration(entries);
+ string filter = "*.*";
+ activeEnumeration.TrySaveFilterString(filter).ShouldEqual(true);
- // "*.*" should only match when there is a . in the name
- activeEnumeration.IsCurrentValid.ShouldEqual(false);
- activeEnumeration.MoveNext().ShouldEqual(false);
- activeEnumeration.RestartEnumeration(filter);
- activeEnumeration.IsCurrentValid.ShouldEqual(false);
- }
+ // "*.*" should only match when there is a . in the name
+ activeEnumeration.IsCurrentValid.ShouldEqual(false);
+ activeEnumeration.Current.ShouldEqual(null);
+
+ activeEnumeration.MoveNext().ShouldEqual(false);
+ activeEnumeration.IsCurrentValid.ShouldEqual(false);
+ activeEnumeration.Current.ShouldEqual(null);
+
+ activeEnumeration.RestartEnumeration(filter);
+ activeEnumeration.IsCurrentValid.ShouldEqual(false);
+ activeEnumeration.Current.ShouldEqual(null);
}
[TestCase]
@@ -140,17 +133,13 @@ public void EnumerateSingleEntryListWithMatchingFilter()
new ProjectedFileInfo("a", size: 0, isFolder:false, sha: new Sha1Id(1, 1, 1))
};
- using (ActiveEnumeration activeEnumeration = new ActiveEnumeration(entries))
- {
- activeEnumeration.TrySaveFilterString("a").ShouldEqual(true);
- this.ValidateActiveEnumeratorReturnsAllEntries(activeEnumeration, entries);
- }
+ ActiveEnumeration activeEnumeration = CreateActiveEnumeration(entries);
+ activeEnumeration.TrySaveFilterString("a").ShouldEqual(true);
+ this.ValidateActiveEnumeratorReturnsAllEntries(activeEnumeration, entries);
- using (ActiveEnumeration activeEnumeration = new ActiveEnumeration(entries))
- {
- activeEnumeration.TrySaveFilterString("A").ShouldEqual(true);
- this.ValidateActiveEnumeratorReturnsAllEntries(activeEnumeration, entries);
- }
+ activeEnumeration = CreateActiveEnumeration(entries);
+ activeEnumeration.TrySaveFilterString("A").ShouldEqual(true);
+ this.ValidateActiveEnumeratorReturnsAllEntries(activeEnumeration, entries);
}
[TestCase]
@@ -161,15 +150,19 @@ public void EnumerateSingleEntryListWithNonMatchingFilter()
new ProjectedFileInfo("a", size: 0, isFolder:false, sha: new Sha1Id(1, 1, 1))
};
- using (ActiveEnumeration activeEnumeration = new ActiveEnumeration(entries))
- {
- string filter = "b";
- activeEnumeration.TrySaveFilterString(filter).ShouldEqual(true);
- activeEnumeration.IsCurrentValid.ShouldEqual(false);
- activeEnumeration.MoveNext().ShouldEqual(false);
- activeEnumeration.RestartEnumeration(filter);
- activeEnumeration.IsCurrentValid.ShouldEqual(false);
- }
+ ActiveEnumeration activeEnumeration = CreateActiveEnumeration(entries);
+ string filter = "b";
+ activeEnumeration.TrySaveFilterString(filter).ShouldEqual(true);
+ activeEnumeration.IsCurrentValid.ShouldEqual(false);
+ activeEnumeration.Current.ShouldEqual(null);
+
+ activeEnumeration.MoveNext().ShouldEqual(false);
+ activeEnumeration.IsCurrentValid.ShouldEqual(false);
+ activeEnumeration.Current.ShouldEqual(null);
+
+ activeEnumeration.RestartEnumeration(filter);
+ activeEnumeration.IsCurrentValid.ShouldEqual(false);
+ activeEnumeration.Current.ShouldEqual(null);
}
[TestCase]
@@ -177,14 +170,12 @@ public void CannotSetMoreThanOneFilter()
{
string filterString = "*.*";
- using (ActiveEnumeration activeEnumeration = new ActiveEnumeration(new List()))
- {
- activeEnumeration.TrySaveFilterString(filterString).ShouldEqual(true);
- activeEnumeration.TrySaveFilterString(null).ShouldEqual(false);
- activeEnumeration.TrySaveFilterString(string.Empty).ShouldEqual(false);
- activeEnumeration.TrySaveFilterString("?").ShouldEqual(false);
- activeEnumeration.GetFilterString().ShouldEqual(filterString);
- }
+ ActiveEnumeration activeEnumeration = CreateActiveEnumeration(new List());
+ activeEnumeration.TrySaveFilterString(filterString).ShouldEqual(true);
+ activeEnumeration.TrySaveFilterString(null).ShouldEqual(false);
+ activeEnumeration.TrySaveFilterString(string.Empty).ShouldEqual(false);
+ activeEnumeration.TrySaveFilterString("?").ShouldEqual(false);
+ activeEnumeration.GetFilterString().ShouldEqual(filterString);
}
[TestCase]
@@ -201,20 +192,14 @@ public void EnumerateMultipleEntryListWithEmptyFilter()
};
// Test empty string ("") filter
- using (ActiveEnumeration activeEnumeration = new ActiveEnumeration(entries))
- {
- activeEnumeration.IsCurrentValid.ShouldEqual(true);
- activeEnumeration.TrySaveFilterString(string.Empty).ShouldEqual(true);
- this.ValidateActiveEnumeratorReturnsAllEntries(activeEnumeration, entries);
- }
+ ActiveEnumeration activeEnumeration = CreateActiveEnumeration(entries);
+ activeEnumeration.TrySaveFilterString(string.Empty).ShouldEqual(true);
+ this.ValidateActiveEnumeratorReturnsAllEntries(activeEnumeration, entries);
// Test null filter
- using (ActiveEnumeration activeEnumeration = new ActiveEnumeration(entries))
- {
- activeEnumeration.IsCurrentValid.ShouldEqual(true);
- activeEnumeration.TrySaveFilterString(null).ShouldEqual(true);
- this.ValidateActiveEnumeratorReturnsAllEntries(activeEnumeration, entries);
- }
+ activeEnumeration = CreateActiveEnumeration(entries);
+ activeEnumeration.TrySaveFilterString(null).ShouldEqual(true);
+ this.ValidateActiveEnumeratorReturnsAllEntries(activeEnumeration, entries);
}
[TestCase]
@@ -233,95 +218,59 @@ public void EnumerateMultipleEntryListWithWildcardFilter()
new ProjectedFileInfo("E.bat", size: 0, isFolder:false, sha: new Sha1Id(1, 1, 8)),
};
- using (ActiveEnumeration activeEnumeration = new ActiveEnumeration(entries))
- {
- activeEnumeration.IsCurrentValid.ShouldEqual(true);
- activeEnumeration.TrySaveFilterString("*").ShouldEqual(true);
- this.ValidateActiveEnumeratorReturnsAllEntries(activeEnumeration, entries);
- }
+ ActiveEnumeration activeEnumeration = CreateActiveEnumeration(entries);
+ activeEnumeration.TrySaveFilterString("*").ShouldEqual(true);
+ this.ValidateActiveEnumeratorReturnsAllEntries(activeEnumeration, entries);
- using (ActiveEnumeration activeEnumeration = new ActiveEnumeration(entries))
- {
- activeEnumeration.IsCurrentValid.ShouldEqual(true);
- activeEnumeration.TrySaveFilterString("*.*").ShouldEqual(true);
- this.ValidateActiveEnumeratorReturnsAllEntries(activeEnumeration, entries.Where(entry => entry.Name.Contains(".")));
- }
+ activeEnumeration = CreateActiveEnumeration(entries);
+ activeEnumeration.TrySaveFilterString("*.*").ShouldEqual(true);
+ this.ValidateActiveEnumeratorReturnsAllEntries(activeEnumeration, entries.Where(entry => entry.Name.Contains(".")));
- using (ActiveEnumeration activeEnumeration = new ActiveEnumeration(entries))
- {
- activeEnumeration.IsCurrentValid.ShouldEqual(true);
- activeEnumeration.TrySaveFilterString("*.txt").ShouldEqual(true);
- this.ValidateActiveEnumeratorReturnsAllEntries(activeEnumeration, entries.Where(entry => entry.Name.EndsWith(".txt", System.StringComparison.OrdinalIgnoreCase)));
- }
+ activeEnumeration = CreateActiveEnumeration(entries);
+ activeEnumeration.TrySaveFilterString("*.txt").ShouldEqual(true);
+ this.ValidateActiveEnumeratorReturnsAllEntries(activeEnumeration, entries.Where(entry => entry.Name.EndsWith(".txt", System.StringComparison.OrdinalIgnoreCase)));
// '<' = DOS_STAR, matches 0 or more characters until encountering and matching
// the final . in the name
- using (ActiveEnumeration activeEnumeration = new ActiveEnumeration(entries))
- {
- activeEnumeration.IsCurrentValid.ShouldEqual(true);
- activeEnumeration.TrySaveFilterString("<.txt").ShouldEqual(true);
- this.ValidateActiveEnumeratorReturnsAllEntries(activeEnumeration, entries.Where(entry => entry.Name.EndsWith(".txt", System.StringComparison.OrdinalIgnoreCase)));
- }
+ activeEnumeration = CreateActiveEnumeration(entries);
+ activeEnumeration.TrySaveFilterString("<.txt").ShouldEqual(true);
+ this.ValidateActiveEnumeratorReturnsAllEntries(activeEnumeration, entries.Where(entry => entry.Name.EndsWith(".txt", System.StringComparison.OrdinalIgnoreCase)));
- using (ActiveEnumeration activeEnumeration = new ActiveEnumeration(entries))
- {
- activeEnumeration.IsCurrentValid.ShouldEqual(true);
- activeEnumeration.TrySaveFilterString("?").ShouldEqual(true);
- this.ValidateActiveEnumeratorReturnsAllEntries(activeEnumeration, entries.Where(entry => entry.Name.Length == 1));
- }
+ activeEnumeration = CreateActiveEnumeration(entries);
+ activeEnumeration.TrySaveFilterString("?").ShouldEqual(true);
+ this.ValidateActiveEnumeratorReturnsAllEntries(activeEnumeration, entries.Where(entry => entry.Name.Length == 1));
- using (ActiveEnumeration activeEnumeration = new ActiveEnumeration(entries))
- {
- activeEnumeration.IsCurrentValid.ShouldEqual(true);
- activeEnumeration.TrySaveFilterString("?.txt").ShouldEqual(true);
- this.ValidateActiveEnumeratorReturnsAllEntries(activeEnumeration, entries.Where(entry => entry.Name.Length == 5 && entry.Name.EndsWith(".txt", System.StringComparison.OrdinalIgnoreCase)));
- }
+ 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)));
// '>' = 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.
- using (ActiveEnumeration activeEnumeration = new ActiveEnumeration(entries))
- {
- activeEnumeration.IsCurrentValid.ShouldEqual(true);
- activeEnumeration.TrySaveFilterString(">.txt").ShouldEqual(true);
- this.ValidateActiveEnumeratorReturnsAllEntries(activeEnumeration, entries.Where(entry => entry.Name.Length <= 5 && entry.Name.EndsWith(".txt", System.StringComparison.OrdinalIgnoreCase)));
- }
+ 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)));
- using (ActiveEnumeration activeEnumeration = new ActiveEnumeration(entries))
- {
- activeEnumeration.IsCurrentValid.ShouldEqual(true);
- activeEnumeration.TrySaveFilterString("E.???").ShouldEqual(true);
- this.ValidateActiveEnumeratorReturnsAllEntries(activeEnumeration, entries.Where(entry => entry.Name.Length == 5 && entry.Name.StartsWith("E.", System.StringComparison.OrdinalIgnoreCase)));
- }
+ 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)));
// '"' = DOS_DOT, matches either a . or zero characters beyond name string.
- using (ActiveEnumeration activeEnumeration = new ActiveEnumeration(entries))
- {
- activeEnumeration.IsCurrentValid.ShouldEqual(true);
- 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)));
- }
+ 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)));
- using (ActiveEnumeration activeEnumeration = new ActiveEnumeration(entries))
- {
- activeEnumeration.IsCurrentValid.ShouldEqual(true);
- 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)));
- }
+ 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)));
- using (ActiveEnumeration activeEnumeration = new ActiveEnumeration(entries))
- {
- activeEnumeration.IsCurrentValid.ShouldEqual(true);
- 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)));
- }
+ 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)));
- using (ActiveEnumeration activeEnumeration = new ActiveEnumeration(entries))
- {
- activeEnumeration.IsCurrentValid.ShouldEqual(true);
- activeEnumeration.TrySaveFilterString("e.???").ShouldEqual(true);
- this.ValidateActiveEnumeratorReturnsAllEntries(activeEnumeration, entries.Where(entry => entry.Name.Length == 5 && entry.Name.StartsWith("E.", System.StringComparison.OrdinalIgnoreCase)));
- }
+ 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)));
}
[TestCase]
@@ -337,19 +286,13 @@ public void EnumerateMultipleEntryListWithMatchingFilter()
new ProjectedFileInfo("E.bat", size: 0, isFolder:false, sha: new Sha1Id(1, 1, 5)),
};
- using (ActiveEnumeration activeEnumeration = new ActiveEnumeration(entries))
- {
- activeEnumeration.IsCurrentValid.ShouldEqual(true);
- activeEnumeration.TrySaveFilterString("E.bat").ShouldEqual(true);
- this.ValidateActiveEnumeratorReturnsAllEntries(activeEnumeration, entries.Where(entry => entry.Name == "E.bat"));
- }
+ ActiveEnumeration activeEnumeration = CreateActiveEnumeration(entries);
+ activeEnumeration.TrySaveFilterString("E.bat").ShouldEqual(true);
+ this.ValidateActiveEnumeratorReturnsAllEntries(activeEnumeration, entries.Where(entry => entry.Name == "E.bat"));
- using (ActiveEnumeration activeEnumeration = new ActiveEnumeration(entries))
- {
- activeEnumeration.IsCurrentValid.ShouldEqual(true);
- activeEnumeration.TrySaveFilterString("e.bat").ShouldEqual(true);
- this.ValidateActiveEnumeratorReturnsAllEntries(activeEnumeration, entries.Where(entry => string.Compare(entry.Name, "e.bat", StringComparison.OrdinalIgnoreCase) == 0));
- }
+ activeEnumeration = CreateActiveEnumeration(entries);
+ activeEnumeration.TrySaveFilterString("e.bat").ShouldEqual(true);
+ this.ValidateActiveEnumeratorReturnsAllEntries(activeEnumeration, entries.Where(entry => string.Compare(entry.Name, "e.bat", StringComparison.OrdinalIgnoreCase) == 0));
}
[TestCase]
@@ -365,15 +308,13 @@ public void EnumerateMultipleEntryListWithNonMatchingFilter()
new ProjectedFileInfo("E.bat", size: 0, isFolder:false, sha: new Sha1Id(1, 1, 5)),
};
- using (ActiveEnumeration activeEnumeration = new ActiveEnumeration(entries))
- {
- string filter = "g";
- activeEnumeration.TrySaveFilterString(filter).ShouldEqual(true);
- activeEnumeration.IsCurrentValid.ShouldEqual(false);
- activeEnumeration.MoveNext().ShouldEqual(false);
- activeEnumeration.RestartEnumeration(filter);
- activeEnumeration.IsCurrentValid.ShouldEqual(false);
- }
+ ActiveEnumeration activeEnumeration = CreateActiveEnumeration(entries);
+ string filter = "g";
+ activeEnumeration.TrySaveFilterString(filter).ShouldEqual(true);
+ activeEnumeration.IsCurrentValid.ShouldEqual(false);
+ activeEnumeration.MoveNext().ShouldEqual(false);
+ activeEnumeration.RestartEnumeration(filter);
+ activeEnumeration.IsCurrentValid.ShouldEqual(false);
}
[TestCase]
@@ -389,14 +330,10 @@ public void SettingFilterAdvancesEnumeratorToMatchingEntry()
new ProjectedFileInfo("E.bat", size: 0, isFolder:false, sha: new Sha1Id(1, 1, 5)),
};
- using (ActiveEnumeration activeEnumeration = new ActiveEnumeration(entries))
- {
- activeEnumeration.IsCurrentValid.ShouldEqual(true);
- activeEnumeration.Current.ShouldBeSameAs(entries[0]);
- activeEnumeration.TrySaveFilterString("D.txt").ShouldEqual(true);
- activeEnumeration.IsCurrentValid.ShouldEqual(true);
- activeEnumeration.Current.Name.ShouldEqual("D.txt");
- }
+ ActiveEnumeration activeEnumeration = CreateActiveEnumeration(entries);
+ activeEnumeration.TrySaveFilterString("D.txt").ShouldEqual(true);
+ activeEnumeration.IsCurrentValid.ShouldEqual(true);
+ activeEnumeration.Current.Name.ShouldEqual("D.txt");
}
[TestCase]
@@ -412,18 +349,14 @@ public void RestartingScanWithFilterAdvancesEnumeratorToNewMatchingEntry()
new ProjectedFileInfo("E.bat", size: 0, isFolder:false, sha: new Sha1Id(1, 1, 5)),
};
- using (ActiveEnumeration activeEnumeration = new ActiveEnumeration(entries))
- {
- activeEnumeration.IsCurrentValid.ShouldEqual(true);
- activeEnumeration.Current.ShouldBeSameAs(entries[0]);
- activeEnumeration.TrySaveFilterString("D.txt").ShouldEqual(true);
- activeEnumeration.IsCurrentValid.ShouldEqual(true);
- activeEnumeration.Current.Name.ShouldEqual("D.txt");
+ ActiveEnumeration activeEnumeration = CreateActiveEnumeration(entries);
+ activeEnumeration.TrySaveFilterString("D.txt").ShouldEqual(true);
+ activeEnumeration.IsCurrentValid.ShouldEqual(true);
+ activeEnumeration.Current.Name.ShouldEqual("D.txt");
- activeEnumeration.RestartEnumeration("c");
- activeEnumeration.IsCurrentValid.ShouldEqual(true);
- activeEnumeration.Current.Name.ShouldEqual("c");
- }
+ activeEnumeration.RestartEnumeration("c");
+ activeEnumeration.IsCurrentValid.ShouldEqual(true);
+ activeEnumeration.Current.Name.ShouldEqual("c");
}
[TestCase]
@@ -437,18 +370,31 @@ public void RestartingScanWithFilterAdvancesEnumeratorToFirstMatchingEntry()
new ProjectedFileInfo("E.bat", size: 0, isFolder:false, sha: new Sha1Id(1, 1, 4)),
};
- using (ActiveEnumeration activeEnumeration = new ActiveEnumeration(entries))
+ ActiveEnumeration activeEnumeration = CreateActiveEnumeration(entries);
+ activeEnumeration.TrySaveFilterString("D.txt").ShouldEqual(true);
+ activeEnumeration.IsCurrentValid.ShouldEqual(true);
+ activeEnumeration.Current.Name.ShouldEqual("D.txt");
+
+ activeEnumeration.RestartEnumeration("c*");
+ activeEnumeration.IsCurrentValid.ShouldEqual(true);
+ activeEnumeration.Current.Name.ShouldEqual("C.TXT");
+ }
+
+ private static ActiveEnumeration CreateActiveEnumeration(List entries)
+ {
+ ActiveEnumeration activeEnumeration = new ActiveEnumeration(entries);
+ if (entries.Count > 0)
{
activeEnumeration.IsCurrentValid.ShouldEqual(true);
activeEnumeration.Current.ShouldBeSameAs(entries[0]);
- activeEnumeration.TrySaveFilterString("D.txt").ShouldEqual(true);
- activeEnumeration.IsCurrentValid.ShouldEqual(true);
- activeEnumeration.Current.Name.ShouldEqual("D.txt");
-
- activeEnumeration.RestartEnumeration("c*");
- activeEnumeration.IsCurrentValid.ShouldEqual(true);
- activeEnumeration.Current.Name.ShouldEqual("C.TXT");
}
+ else
+ {
+ activeEnumeration.IsCurrentValid.ShouldEqual(false);
+ activeEnumeration.Current.ShouldBeNull();
+ }
+
+ return activeEnumeration;
}
private void ValidateActiveEnumeratorReturnsAllEntries(ActiveEnumeration activeEnumeration, IEnumerable entries)
@@ -465,9 +411,12 @@ private void ValidateActiveEnumeratorReturnsAllEntries(ActiveEnumeration activeE
// activeEnumeration should no longer be valid after iterating beyond the end of the list
activeEnumeration.IsCurrentValid.ShouldEqual(false);
+ activeEnumeration.Current.ShouldBeNull();
// attempts to move beyond the end of the list should fail
activeEnumeration.MoveNext().ShouldEqual(false);
+ activeEnumeration.IsCurrentValid.ShouldEqual(false);
+ activeEnumeration.Current.ShouldBeNull();
}
public class PatternMatcherWrapper
diff --git a/GVFS/GVFS.UnitTests/Common/CacheServerResolverTests.cs b/GVFS/GVFS.UnitTests/Common/CacheServerResolverTests.cs
index 185b1b77a0..c46d4b9669 100644
--- a/GVFS/GVFS.UnitTests/Common/CacheServerResolverTests.cs
+++ b/GVFS/GVFS.UnitTests/Common/CacheServerResolverTests.cs
@@ -18,7 +18,7 @@ public class CacheServerResolverTests
[TestCase]
public void CanGetCacheServerFromNewConfig()
{
- MockEnlistment enlistment = this.CreateEnlistment(CacheServerUrl);
+ MockGVFSEnlistment enlistment = this.CreateEnlistment(CacheServerUrl);
CacheServerInfo cacheServer = CacheServerResolver.GetCacheServerFromConfig(enlistment);
cacheServer.Url.ShouldEqual(CacheServerUrl);
@@ -28,7 +28,7 @@ public void CanGetCacheServerFromNewConfig()
[TestCase]
public void CanGetCacheServerFromOldConfig()
{
- MockEnlistment enlistment = this.CreateEnlistment(null, CacheServerUrl);
+ MockGVFSEnlistment enlistment = this.CreateEnlistment(null, CacheServerUrl);
CacheServerInfo cacheServer = CacheServerResolver.GetCacheServerFromConfig(enlistment);
cacheServer.Url.ShouldEqual(CacheServerUrl);
@@ -38,7 +38,7 @@ public void CanGetCacheServerFromOldConfig()
[TestCase]
public void CanGetCacheServerWithNoConfig()
{
- MockEnlistment enlistment = this.CreateEnlistment();
+ MockGVFSEnlistment enlistment = this.CreateEnlistment();
this.ValidateIsNone(enlistment, CacheServerResolver.GetCacheServerFromConfig(enlistment));
CacheServerResolver.GetUrlFromConfig(enlistment).ShouldEqual(enlistment.RepoUrl);
@@ -82,7 +82,7 @@ public void CanResolveNameFromCustomUrl()
[TestCase]
public void CanResolveUrlAsRepoUrl()
{
- MockEnlistment enlistment = this.CreateEnlistment();
+ MockGVFSEnlistment enlistment = this.CreateEnlistment();
CacheServerResolver resolver = this.CreateResolver(enlistment);
this.ValidateIsNone(enlistment, resolver.ResolveNameFromRemote(enlistment.RepoUrl, this.CreateGVFSConfig()));
@@ -134,7 +134,7 @@ public void CanParseAndResolveDefault()
[TestCase]
public void CanParseAndResolveNoCacheServer()
{
- MockEnlistment enlistment = this.CreateEnlistment();
+ MockGVFSEnlistment enlistment = this.CreateEnlistment();
CacheServerResolver resolver = this.CreateResolver(enlistment);
this.ValidateIsNone(enlistment, resolver.ParseUrlOrFriendlyName(CacheServerInfo.ReservedNames.None));
@@ -159,7 +159,7 @@ public void CanParseAndResolveNoCacheServer()
[TestCase]
public void CanParseAndResolveDefaultWhenServerAdvertisesNullListOfCacheServers()
{
- MockEnlistment enlistment = this.CreateEnlistment();
+ MockGVFSEnlistment enlistment = this.CreateEnlistment();
CacheServerResolver resolver = this.CreateResolver(enlistment);
CacheServerInfo resolvedCacheServer;
@@ -173,7 +173,7 @@ public void CanParseAndResolveDefaultWhenServerAdvertisesNullListOfCacheServers(
[TestCase]
public void CanParseAndResolveOtherWhenServerAdvertisesNullListOfCacheServers()
{
- MockEnlistment enlistment = this.CreateEnlistment();
+ MockGVFSEnlistment enlistment = this.CreateEnlistment();
CacheServerResolver resolver = this.CreateResolver(enlistment);
CacheServerInfo resolvedCacheServer;
@@ -191,7 +191,7 @@ private void ValidateIsNone(Enlistment enlistment, CacheServerInfo cacheServer)
cacheServer.Name.ShouldEqual(CacheServerInfo.ReservedNames.None);
}
- private MockEnlistment CreateEnlistment(string newConfigValue = null, string oldConfigValue = null)
+ private MockGVFSEnlistment CreateEnlistment(string newConfigValue = null, string oldConfigValue = null)
{
MockGitProcess gitProcess = new MockGitProcess();
gitProcess.SetExpectedCommandResult(
@@ -201,7 +201,7 @@ private MockEnlistment CreateEnlistment(string newConfigValue = null, string old
"config gvfs.mock:..repourl.cache-server-url",
() => new GitProcess.Result(oldConfigValue ?? string.Empty, string.Empty, oldConfigValue != null ? GitProcess.Result.SuccessCode : GitProcess.Result.GenericFailureCode));
- return new MockEnlistment(gitProcess);
+ return new MockGVFSEnlistment(gitProcess);
}
private GVFSConfig CreateGVFSConfig()
@@ -220,7 +220,7 @@ private GVFSConfig CreateDefaultDeserializedGVFSConfig()
return JsonConvert.DeserializeObject("{}");
}
- private CacheServerResolver CreateResolver(MockEnlistment enlistment = null)
+ private CacheServerResolver CreateResolver(MockGVFSEnlistment enlistment = null)
{
enlistment = enlistment ?? this.CreateEnlistment();
return new CacheServerResolver(new MockTracer(), enlistment);
diff --git a/GVFS/GVFS.UnitTests/Common/GitCommandLineParserTests.cs b/GVFS/GVFS.UnitTests/Common/GitCommandLineParserTests.cs
index 05122d87bd..1000f98d62 100644
--- a/GVFS/GVFS.UnitTests/Common/GitCommandLineParserTests.cs
+++ b/GVFS/GVFS.UnitTests/Common/GitCommandLineParserTests.cs
@@ -83,5 +83,18 @@ public void IsCheckoutWithFilePathsTests()
new GitCommandLineParser("git checkout HEAD --").IsCheckoutWithFilePaths().ShouldEqual(false);
new GitCommandLineParser("git checkout HEAD -- ").IsCheckoutWithFilePaths().ShouldEqual(false);
}
+
+ [TestCase]
+ public void IsSerializedStatusTests()
+ {
+ new GitCommandLineParser("git status --serialized=some/file").IsSerializedStatus().ShouldEqual(true);
+ new GitCommandLineParser("git status --serialized").IsSerializedStatus().ShouldEqual(true);
+
+ new GitCommandLineParser("git checkout branch -- file").IsSerializedStatus().ShouldEqual(false);
+ new GitCommandLineParser("git status").IsSerializedStatus().ShouldEqual(false);
+ new GitCommandLineParser("git checkout --serialized").IsSerializedStatus().ShouldEqual(false);
+ new GitCommandLineParser("git checkout --serialized=some/file").IsSerializedStatus().ShouldEqual(false);
+ new GitCommandLineParser("gits status --serialized=some/file").IsSerializedStatus().ShouldEqual(false);
+ }
}
}
diff --git a/GVFS/GVFS.UnitTests/Common/GitStatusCacheTests.cs b/GVFS/GVFS.UnitTests/Common/GitStatusCacheTests.cs
new file mode 100644
index 0000000000..a4ef83db63
--- /dev/null
+++ b/GVFS/GVFS.UnitTests/Common/GitStatusCacheTests.cs
@@ -0,0 +1,156 @@
+using GVFS.Common;
+using GVFS.Common.Git;
+using GVFS.Common.NamedPipes;
+using GVFS.Tests.Should;
+using GVFS.UnitTests.Mock.Common;
+using GVFS.UnitTests.Mock.FileSystem;
+using GVFS.UnitTests.Mock.Git;
+using NUnit.Framework;
+using System;
+using System.IO;
+
+namespace GVFS.UnitTests.Common
+{
+ [TestFixture]
+ public class GitStatusCacheTests
+ {
+ private static NamedPipeMessages.LockData statusCommandLockData = new NamedPipeMessages.LockData(123, false, false, "git status");
+
+ private MockFileSystem fileSystem;
+ private MockGitProcess gitProcess;
+ private GVFSContext context;
+ private string gitParentPath;
+ private string gvfsMetadataPath;
+ private MockDirectory enlistmentDirectory;
+
+ [SetUp]
+ public void SetUp()
+ {
+ MockTracer tracer = new MockTracer();
+
+ string enlistmentRoot = Path.Combine("mock:", "GVFS", "UnitTests", "Repo");
+ string statusCachePath = Path.Combine("mock:", "GVFS", "UnitTests", "Repo", ".gvfs", "gitStatusCache");
+
+ this.gitProcess = new MockGitProcess();
+ this.gitProcess.SetExpectedCommandResult($"--no-optional-locks status \"--serialize={statusCachePath}", () => new GitProcess.Result(string.Empty, string.Empty, 0), true);
+ MockGVFSEnlistment enlistment = new MockGVFSEnlistment(enlistmentRoot, "fake://repoUrl", "fake://gitBinPath", null, this.gitProcess);
+ enlistment.InitializeCachePathsFromKey("fake:\\gvfsSharedCache", "fakeCacheKey");
+
+ this.gitParentPath = enlistment.WorkingDirectoryRoot;
+ this.gvfsMetadataPath = enlistment.DotGVFSRoot;
+
+ this.enlistmentDirectory = new MockDirectory(
+ enlistmentRoot,
+ new MockDirectory[]
+ {
+ new MockDirectory(this.gitParentPath, folders: null, files: null),
+ },
+ null);
+
+ this.enlistmentDirectory.CreateFile(Path.Combine(this.gitParentPath, ".git", "config"), ".git config Contents", createDirectories: true);
+ this.enlistmentDirectory.CreateFile(Path.Combine(this.gitParentPath, ".git", "HEAD"), ".git HEAD Contents", createDirectories: true);
+ this.enlistmentDirectory.CreateFile(Path.Combine(this.gitParentPath, ".git", "logs", "HEAD"), "HEAD Contents", createDirectories: true);
+ this.enlistmentDirectory.CreateFile(Path.Combine(this.gitParentPath, ".git", "info", "always_exclude"), "always_exclude Contents", createDirectories: true);
+ this.enlistmentDirectory.CreateDirectory(Path.Combine(this.gitParentPath, ".git", "objects", "pack"));
+
+ this.fileSystem = new MockFileSystem(this.enlistmentDirectory);
+ this.fileSystem.AllowMoveFile = true;
+ this.fileSystem.DeleteNonExistentFileThrowsException = false;
+
+ this.context = new GVFSContext(
+ tracer,
+ this.fileSystem,
+ new MockGitRepo(tracer, enlistment, this.fileSystem),
+ enlistment);
+ }
+
+ [TearDown]
+ public void TearDown()
+ {
+ this.fileSystem = null;
+ this.gitProcess = null;
+ this.context = null;
+ this.gitParentPath = null;
+ this.gvfsMetadataPath = null;
+ this.enlistmentDirectory = null;
+ }
+
+ [TestCase]
+ public void CanInvalidateCleanCache()
+ {
+ this.enlistmentDirectory.CreateFile(Path.Combine(this.gvfsMetadataPath, GVFSConstants.DotGVFS.GitStatusCache.CachePath), "Git status cache contents", createDirectories: true);
+ using (GitStatusCache statusCache = new GitStatusCache(this.context, TimeSpan.Zero))
+ {
+ statusCache.Initialize();
+ statusCache.IsCacheReadyAndUpToDate().ShouldBeFalse();
+
+ // Refresh the cache to put it into the clean state.
+ statusCache.RefreshAndWait();
+
+ bool result = statusCache.IsReadyForExternalAcquireLockRequests(statusCommandLockData, out _);
+
+ result.ShouldBeTrue();
+ statusCache.IsCacheReadyAndUpToDate().ShouldBeTrue();
+
+ // Invalidate the cache, and make sure that it transistions into
+ // the dirty state, and that commands are still allowed through.
+ statusCache.Invalidate();
+ statusCache.IsCacheReadyAndUpToDate().ShouldBeFalse();
+
+ result = statusCache.IsReadyForExternalAcquireLockRequests(statusCommandLockData, out _);
+ result.ShouldBeTrue();
+
+ // After checking if we are ready for external lock requests, cache should still be dirty
+ statusCache.IsCacheReadyAndUpToDate().ShouldBeFalse();
+
+ statusCache.Shutdown();
+ }
+ }
+
+ [TestCase]
+ public void CacheFileErrorShouldBlock()
+ {
+ this.fileSystem.DeleteFileThrowsException = true;
+ this.enlistmentDirectory.CreateFile(Path.Combine(this.gvfsMetadataPath, GVFSConstants.DotGVFS.GitStatusCache.CachePath), "Git status cache contents", createDirectories: true);
+
+ using (GitStatusCache statusCache = new GitStatusCache(this.context, TimeSpan.Zero))
+ {
+ statusCache.Initialize();
+
+ statusCache.IsCacheReadyAndUpToDate().ShouldBeFalse();
+
+ bool isReady = statusCache.IsReadyForExternalAcquireLockRequests(statusCommandLockData, out _);
+ isReady.ShouldBeFalse();
+
+ statusCache.IsCacheReadyAndUpToDate().ShouldBeFalse();
+
+ statusCache.Shutdown();
+ }
+ }
+
+ [TestCase]
+ public void CanRefreshCache()
+ {
+ this.enlistmentDirectory.CreateFile(Path.Combine(this.gvfsMetadataPath, GVFSConstants.DotGVFS.GitStatusCache.CachePath), "Git status cache contents", createDirectories: true);
+ using (GitStatusCache statusCache = new GitStatusCache(this.context, TimeSpan.Zero))
+ {
+ statusCache.Initialize();
+
+ statusCache.IsCacheReadyAndUpToDate().ShouldBeFalse();
+
+ string message;
+ bool result = statusCache.IsReadyForExternalAcquireLockRequests(statusCommandLockData, out message);
+ result.ShouldBeTrue();
+
+ statusCache.RefreshAndWait();
+
+ result = statusCache.IsReadyForExternalAcquireLockRequests(statusCommandLockData, out message);
+ result.ShouldBeTrue();
+
+ statusCache.IsCacheReadyAndUpToDate().ShouldBeTrue();
+
+ statusCache.Shutdown();
+ }
+ }
+ }
+}
diff --git a/GVFS/GVFS.UnitTests/Git/GVFSGitObjectsTests.cs b/GVFS/GVFS.UnitTests/Git/GVFSGitObjectsTests.cs
index 0c7d448ced..016a6e3ce5 100644
--- a/GVFS/GVFS.UnitTests/Git/GVFSGitObjectsTests.cs
+++ b/GVFS/GVFS.UnitTests/Git/GVFSGitObjectsTests.cs
@@ -156,11 +156,11 @@ private string GetDataPath(string fileName)
private class MockHttpGitObjects : GitObjectsHttpRequestor
{
public MockHttpGitObjects()
- : this(new MockEnlistment())
+ : this(new MockGVFSEnlistment())
{
}
- private MockHttpGitObjects(MockEnlistment enlistment)
+ private MockHttpGitObjects(MockGVFSEnlistment enlistment)
: base(new MockTracer(), enlistment, new MockCacheServerInfo(), new RetryConfig(maxRetries: 1))
{
}
diff --git a/GVFS/GVFS.UnitTests/Mock/Common/MockEnlistment.cs b/GVFS/GVFS.UnitTests/Mock/Common/MockGVFSEnlistment.cs
similarity index 60%
rename from GVFS/GVFS.UnitTests/Mock/Common/MockEnlistment.cs
rename to GVFS/GVFS.UnitTests/Mock/Common/MockGVFSEnlistment.cs
index 264a62b467..75e8028371 100644
--- a/GVFS/GVFS.UnitTests/Mock/Common/MockEnlistment.cs
+++ b/GVFS/GVFS.UnitTests/Mock/Common/MockGVFSEnlistment.cs
@@ -4,19 +4,25 @@
namespace GVFS.UnitTests.Mock.Common
{
- public class MockEnlistment : Enlistment
+ public class MockGVFSEnlistment : GVFSEnlistment
{
private MockGitProcess gitProcess;
- public MockEnlistment()
- : base("mock:\\path", "mock:\\path", "mock://repoUrl", "mock:\\git", null, flushFileBuffersForPacks: false)
+ public MockGVFSEnlistment()
+ : base("mock:\\path", "mock://repoUrl", "mock:\\git", null)
{
this.GitObjectsRoot = "mock:\\path\\.git\\objects";
this.LocalObjectsRoot = this.GitObjectsRoot;
this.GitPackRoot = "mock:\\path\\.git\\objects\\pack";
}
- public MockEnlistment(MockGitProcess gitProcess)
+ public MockGVFSEnlistment(string enlistmentRoot, string repoUrl, string gitBinPath, string gvfsHooksRoot, MockGitProcess gitProcess)
+ : base(enlistmentRoot, repoUrl, gitBinPath, gvfsHooksRoot)
+ {
+ this.gitProcess = gitProcess;
+ }
+
+ public MockGVFSEnlistment(MockGitProcess gitProcess)
: this()
{
this.gitProcess = gitProcess;
diff --git a/GVFS/GVFS.UnitTests/Mock/Common/MockGitStatusCache.cs b/GVFS/GVFS.UnitTests/Mock/Common/MockGitStatusCache.cs
new file mode 100644
index 0000000000..4f4a27dd38
--- /dev/null
+++ b/GVFS/GVFS.UnitTests/Mock/Common/MockGitStatusCache.cs
@@ -0,0 +1,59 @@
+using GVFS.Common;
+using GVFS.Common.NamedPipes;
+using GVFS.Common.Tracing;
+using System;
+
+namespace GVFS.UnitTests.Mock.Common
+{
+ public class MockGitStatusCache : GitStatusCache
+ {
+ public MockGitStatusCache(GVFSContext context, TimeSpan backoff)
+ : base(context, backoff)
+ {
+ }
+
+ public int InvalidateCallCount { get; private set; }
+
+ public void ResetCalls()
+ {
+ this.InvalidateCallCount = 0;
+ }
+
+ public override void Dispose()
+ {
+ }
+
+ public override void Initialize()
+ {
+ }
+
+ public override void Invalidate()
+ {
+ this.InvalidateCallCount++;
+ }
+
+ public override bool IsReadyForExternalAcquireLockRequests(NamedPipeMessages.LockData requester, out string infoMessage)
+ {
+ infoMessage = string.Empty;
+ return true;
+ }
+
+ public override bool IsCacheReadyAndUpToDate()
+ {
+ return false;
+ }
+
+ public override void RefreshAsynchronously()
+ {
+ }
+
+ public override void Shutdown()
+ {
+ }
+
+ public override bool WriteTelemetryandReset(EventMetadata metadata)
+ {
+ return false;
+ }
+ }
+}
diff --git a/GVFS/GVFS.UnitTests/Mock/Common/MockPlatform.cs b/GVFS/GVFS.UnitTests/Mock/Common/MockPlatform.cs
index 82f9146104..973be36b28 100644
--- a/GVFS/GVFS.UnitTests/Mock/Common/MockPlatform.cs
+++ b/GVFS/GVFS.UnitTests/Mock/Common/MockPlatform.cs
@@ -99,5 +99,10 @@ public override void StartBackgroundProcess(string programName, string[] args)
{
throw new NotSupportedException();
}
+
+ public override bool IsGitStatusCacheSupported()
+ {
+ return true;
+ }
}
}
diff --git a/GVFS/GVFS.UnitTests/Mock/FileSystem/MockFileSystem.cs b/GVFS/GVFS.UnitTests/Mock/FileSystem/MockFileSystem.cs
index e19a814905..0c612fe769 100644
--- a/GVFS/GVFS.UnitTests/Mock/FileSystem/MockFileSystem.cs
+++ b/GVFS/GVFS.UnitTests/Mock/FileSystem/MockFileSystem.cs
@@ -13,10 +13,27 @@ public class MockFileSystem : PhysicalFileSystem
public MockFileSystem(MockDirectory rootDirectory)
{
this.RootDirectory = rootDirectory;
+ this.DeleteNonExistentFileThrowsException = true;
}
public MockDirectory RootDirectory { get; private set; }
+ public bool DeleteFileThrowsException { get; set; }
+
+ ///
+ /// Allow FileMoves without checking the input arguments.
+ /// This is to support tests that just want to allow arbitrary
+ /// MoveFile calls to succeed.
+ ///
+ public bool AllowMoveFile { get; set; }
+
+ ///
+ /// Normal behavior C# File.Delete(..) is to not throw if the file to
+ /// be deleted does not exist. However, existing behavior of this mock
+ /// is to throw. This flag allows consumers to control this behavior.
+ ///
+ public bool DeleteNonExistentFileThrowsException { get; set; }
+
public override bool FileExists(string path)
{
return this.RootDirectory.FindFile(path) != null;
@@ -29,7 +46,18 @@ public override void CopyFile(string sourcePath, string destinationPath, bool ov
public override void DeleteFile(string path)
{
+ if (this.DeleteFileThrowsException)
+ {
+ throw new IOException("Exception when deleting file");
+ }
+
MockFile file = this.RootDirectory.FindFile(path);
+
+ if (file == null && !this.DeleteNonExistentFileThrowsException)
+ {
+ return;
+ }
+
file.ShouldNotBeNull();
this.RootDirectory.RemoveFile(path);
@@ -42,12 +70,17 @@ public override void MoveAndOverwriteFile(string sourcePath, string destinationP
throw new ArgumentNullException();
}
+ if (this.AllowMoveFile)
+ {
+ return;
+ }
+
MockFile sourceFile = this.RootDirectory.FindFile(sourcePath);
MockFile destinationFile = this.RootDirectory.FindFile(destinationPath);
if (sourceFile == null)
{
throw new FileNotFoundException();
- }
+ }
if (destinationFile != null)
{
@@ -181,7 +214,14 @@ public override void SetAttributes(string path, FileAttributes fileAttributes)
public override void MoveFile(string sourcePath, string targetPath)
{
- throw new NotImplementedException();
+ if (this.AllowMoveFile)
+ {
+ return;
+ }
+ else
+ {
+ throw new NotImplementedException();
+ }
}
public override string[] GetFiles(string directoryPath, string mask)
diff --git a/GVFS/GVFS.UnitTests/Mock/FileSystem/MockFileSystemCallbacks.cs b/GVFS/GVFS.UnitTests/Mock/FileSystem/MockFileSystemCallbacks.cs
index 2483e69c68..f75396a912 100644
--- a/GVFS/GVFS.UnitTests/Mock/FileSystem/MockFileSystemCallbacks.cs
+++ b/GVFS/GVFS.UnitTests/Mock/FileSystem/MockFileSystemCallbacks.cs
@@ -15,7 +15,7 @@ public MockFileSystemCallbacks(
GVFSGitObjects gitObjects,
RepoMetadata repoMetadata,
FileSystemVirtualizer fileSystemVirtualizer)
- : base(context, gitObjects, repoMetadata, fileSystemVirtualizer)
+ : base(context, gitObjects, repoMetadata, fileSystemVirtualizer, null)
{
}
diff --git a/GVFS/GVFS.UnitTests/Mock/Git/MockGitProcess.cs b/GVFS/GVFS.UnitTests/Mock/Git/MockGitProcess.cs
index 49ffe95352..efd084d0e9 100644
--- a/GVFS/GVFS.UnitTests/Mock/Git/MockGitProcess.cs
+++ b/GVFS/GVFS.UnitTests/Mock/Git/MockGitProcess.cs
@@ -1,6 +1,8 @@
-using GVFS.Common.Git;
+using GVFS.Common.FileSystem;
+using GVFS.Common.Git;
using GVFS.Tests.Should;
using GVFS.UnitTests.Mock.Common;
+using GVFS.UnitTests.Mock.FileSystem;
using System;
using System.Collections.Generic;
using System.IO;
@@ -9,18 +11,19 @@ namespace GVFS.UnitTests.Mock.Git
{
public class MockGitProcess : GitProcess
{
- private Dictionary> expectedCommands = new Dictionary>();
+ private List expectedCommandInfos = new List();
- public MockGitProcess()
- : base(new MockEnlistment())
+ public MockGitProcess()
+ : base(new MockGVFSEnlistment())
{
}
public bool ShouldFail { get; set; }
- public void SetExpectedCommandResult(string command, Func result)
+ public void SetExpectedCommandResult(string command, Func result, bool matchPrefix = false)
{
- this.expectedCommands[command] = result;
+ CommandInfo commandInfo = new CommandInfo(command, result, matchPrefix);
+ this.expectedCommandInfos.Add(commandInfo);
}
protected override Result InvokeGitImpl(string command, string workingDirectory, string dotGitDirectory, bool useReadObjectHook, Action writeStdIn, Action parseStdOutLine, int timeoutMs)
@@ -30,9 +33,39 @@ protected override Result InvokeGitImpl(string command, string workingDirectory,
return new Result(string.Empty, string.Empty, Result.GenericFailureCode);
}
- Func result;
- this.expectedCommands.TryGetValue(command, out result).ShouldEqual(true, "Unexpected command: " + command);
- return result();
+ Predicate commandMatchFunction =
+ (CommandInfo commandInfo) =>
+ {
+ if (commandInfo.MatchPrefix)
+ {
+ return command.StartsWith(commandInfo.Command);
+ }
+ else
+ {
+ return string.Equals(command, commandInfo.Command, StringComparison.Ordinal);
+ }
+ };
+
+ CommandInfo matchedCommand = this.expectedCommandInfos.Find(commandMatchFunction);
+ matchedCommand.ShouldNotBeNull("Unexpected command: " + command);
+
+ return matchedCommand.Result();
+ }
+
+ private class CommandInfo
+ {
+ public CommandInfo(string command, Func result, bool matchPrefix)
+ {
+ this.Command = command;
+ this.Result = result;
+ this.MatchPrefix = matchPrefix;
+ }
+
+ public string Command { get; private set; }
+
+ public Func Result { get; private set; }
+
+ public bool MatchPrefix { get; private set; }
}
}
}
diff --git a/GVFS/GVFS.UnitTests/Mock/Virtualization/Background/MockBackgroundTaskManager.cs b/GVFS/GVFS.UnitTests/Mock/Virtualization/Background/MockBackgroundTaskManager.cs
index 2d88261383..c89704462e 100644
--- a/GVFS/GVFS.UnitTests/Mock/Virtualization/Background/MockBackgroundTaskManager.cs
+++ b/GVFS/GVFS.UnitTests/Mock/Virtualization/Background/MockBackgroundTaskManager.cs
@@ -1,10 +1,15 @@
using GVFS.Virtualization.Background;
+using System;
using System.Collections.Generic;
namespace GVFS.UnitTests.Mock.Virtualization.Background
{
public class MockBackgroundFileSystemTaskRunner : BackgroundFileSystemTaskRunner
{
+ private Func preCallback;
+ private Func callback;
+ private Func postCallback;
+
public MockBackgroundFileSystemTaskRunner()
{
this.BackgroundTasks = new List();
@@ -20,6 +25,16 @@ public override int Count
}
}
+ public override void SetCallbacks(
+ Func preCallback,
+ Func callback,
+ Func postCallback)
+ {
+ this.preCallback = preCallback;
+ this.callback = callback;
+ this.postCallback = postCallback;
+ }
+
public override void Start()
{
}
@@ -32,5 +47,18 @@ public override void Enqueue(FileSystemTask backgroundTask)
public override void Shutdown()
{
}
+
+ public void ProcessTasks()
+ {
+ this.preCallback();
+
+ foreach (FileSystemTask task in this.BackgroundTasks)
+ {
+ this.callback(task);
+ }
+
+ this.postCallback();
+ this.BackgroundTasks.Clear();
+ }
}
}
diff --git a/GVFS/GVFS.UnitTests/Mock/Virtualization/Projection/MockGitIndexProjection.cs b/GVFS/GVFS.UnitTests/Mock/Virtualization/Projection/MockGitIndexProjection.cs
index 776a4b9f40..fe82cd2e3d 100644
--- a/GVFS/GVFS.UnitTests/Mock/Virtualization/Projection/MockGitIndexProjection.cs
+++ b/GVFS/GVFS.UnitTests/Mock/Virtualization/Projection/MockGitIndexProjection.cs
@@ -84,6 +84,11 @@ public void WaitForGetProjectedItems()
this.waitForIsPathProjected.WaitOne();
}
+ public override FileSystemTaskResult OpenIndexForRead()
+ {
+ return FileSystemTaskResult.Success;
+ }
+
public void BlockIsPathProjected(bool willWaitForRequest)
{
if (willWaitForRequest)
@@ -141,7 +146,7 @@ public override void InvalidateProjection()
{
}
- public override bool TryGetProjectedItemsFromMemory(string folderPath, out IEnumerable projectedItems)
+ public override bool TryGetProjectedItemsFromMemory(string folderPath, out List projectedItems)
{
if (this.EnumerationInMemory)
{
@@ -164,7 +169,7 @@ public override ushort GetFilePathMode(string path)
return 0;
}
- public override IEnumerable GetProjectedItems(
+ public override List GetProjectedItems(
CancellationToken cancellationToken,
BlobSizes.BlobSizesConnection blobSizesConnection,
string folderPath)
diff --git a/GVFS/GVFS.UnitTests/Prefetch/BatchObjectDownloadJobTests.cs b/GVFS/GVFS.UnitTests/Prefetch/BatchObjectDownloadJobTests.cs
index 868ecc1013..e2b9a5a4c4 100644
--- a/GVFS/GVFS.UnitTests/Prefetch/BatchObjectDownloadJobTests.cs
+++ b/GVFS/GVFS.UnitTests/Prefetch/BatchObjectDownloadJobTests.cs
@@ -50,7 +50,7 @@ public void OnlyRequestsObjectsNotDownloaded()
BlockingCollection output = new BlockingCollection();
MockTracer tracer = new MockTracer();
- MockEnlistment enlistment = new MockEnlistment();
+ MockGVFSEnlistment enlistment = new MockGVFSEnlistment();
MockBatchHttpGitObjects httpObjects = new MockBatchHttpGitObjects(tracer, enlistment, objectResolver);
BatchObjectDownloadJob dut = new BatchObjectDownloadJob(
diff --git a/GVFS/GVFS.UnitTests/Prefetch/DiffHelperTests.cs b/GVFS/GVFS.UnitTests/Prefetch/DiffHelperTests.cs
index acc1072915..e0ebb3eade 100644
--- a/GVFS/GVFS.UnitTests/Prefetch/DiffHelperTests.cs
+++ b/GVFS/GVFS.UnitTests/Prefetch/DiffHelperTests.cs
@@ -44,7 +44,7 @@ public class DiffHelperTests
public void CanParseDiffForwards()
{
MockTracer tracer = new MockTracer();
- DiffHelper diffForwards = new DiffHelper(tracer, new MockEnlistment(), new List(), new List());
+ DiffHelper diffForwards = new DiffHelper(tracer, new MockGVFSEnlistment(), new List(), new List());
diffForwards.ParseDiffFile(GetDataPath("forward.txt"), "xx:\\fakeRepo");
// File added, file edited, file renamed, folder => file, edit-rename file
@@ -70,7 +70,7 @@ public void CanParseDiffForwards()
public void CanParseBackwardsDiff()
{
MockTracer tracer = new MockTracer();
- DiffHelper diffBackwards = new DiffHelper(tracer, new MockEnlistment(), new List(), new List());
+ DiffHelper diffBackwards = new DiffHelper(tracer, new Mock.Common.MockGVFSEnlistment(), new List(), new List());
diffBackwards.ParseDiffFile(GetDataPath("backward.txt"), "xx:\\fakeRepo");
// File > folder, deleted file, edited file, renamed file, rename-edit file
@@ -94,7 +94,7 @@ public void CanParseBackwardsDiff()
public void ParsesCaseChangesAsAdds()
{
MockTracer tracer = new MockTracer();
- DiffHelper diffBackwards = new DiffHelper(tracer, new MockEnlistment(), new List(), new List());
+ DiffHelper diffBackwards = new DiffHelper(tracer, new Mock.Common.MockGVFSEnlistment(), new List(), new List());
diffBackwards.ParseDiffFile(GetDataPath("caseChange.txt"), "xx:\\fakeRepo");
diffBackwards.RequiredBlobs.Count.ShouldEqual(2);
@@ -114,7 +114,7 @@ public void DetectsFailuresInDiffTree()
MockGitProcess gitProcess = new MockGitProcess();
gitProcess.SetExpectedCommandResult("diff-tree -r -t sha1 sha2", () => new GitProcess.Result(string.Empty, string.Empty, 1));
- DiffHelper diffBackwards = new DiffHelper(tracer, new MockEnlistment(), gitProcess, new List(), new List());
+ DiffHelper diffBackwards = new DiffHelper(tracer, new Mock.Common.MockGVFSEnlistment(), gitProcess, new List(), new List());
diffBackwards.PerformDiff("sha1", "sha2");
diffBackwards.HasFailures.ShouldEqual(true);
}
@@ -126,7 +126,7 @@ public void DetectsFailuresInLsTree()
MockGitProcess gitProcess = new MockGitProcess();
gitProcess.SetExpectedCommandResult("ls-tree -r -t sha1", () => new GitProcess.Result(string.Empty, string.Empty, 1));
- DiffHelper diffBackwards = new DiffHelper(tracer, new MockEnlistment(), gitProcess, new List(), new List());
+ DiffHelper diffBackwards = new DiffHelper(tracer, new Mock.Common.MockGVFSEnlistment(), gitProcess, new List(), new List());
diffBackwards.PerformDiff(null, "sha1");
diffBackwards.HasFailures.ShouldEqual(true);
}
diff --git a/GVFS/GVFS.UnitTests/Prefetch/PrefetchTracingTests.cs b/GVFS/GVFS.UnitTests/Prefetch/PrefetchTracingTests.cs
index 5bc47f4392..9983dcd4f4 100644
--- a/GVFS/GVFS.UnitTests/Prefetch/PrefetchTracingTests.cs
+++ b/GVFS/GVFS.UnitTests/Prefetch/PrefetchTracingTests.cs
@@ -20,7 +20,7 @@ public void ErrorsForBatchObjectDownloadJob()
{
using (ITracer tracer = CreateTracer())
{
- MockEnlistment enlistment = new MockEnlistment();
+ MockGVFSEnlistment enlistment = new MockGVFSEnlistment();
MockHttpGitObjects httpGitObjects = new MockHttpGitObjects(tracer, enlistment);
MockPhysicalGitObjects gitObjects = new MockPhysicalGitObjects(tracer, null, enlistment, httpGitObjects);
@@ -45,7 +45,7 @@ public void SuccessForBatchObjectDownloadJob()
{
using (ITracer tracer = CreateTracer())
{
- MockEnlistment enlistment = new MockEnlistment();
+ MockGVFSEnlistment enlistment = new MockGVFSEnlistment();
MockHttpGitObjects httpGitObjects = new MockHttpGitObjects(tracer, enlistment);
httpGitObjects.AddBlobContent(FakeSha, FakeShaContents);
MockPhysicalGitObjects gitObjects = new MockPhysicalGitObjects(tracer, null, enlistment, httpGitObjects);
@@ -73,7 +73,7 @@ public void ErrorsForIndexPackFile()
{
using (ITracer tracer = CreateTracer())
{
- MockEnlistment enlistment = new MockEnlistment();
+ MockGVFSEnlistment enlistment = new MockGVFSEnlistment();
MockPhysicalGitObjects gitObjects = new MockPhysicalGitObjects(tracer, null, enlistment, null);
BlockingCollection input = new BlockingCollection();
diff --git a/GVFS/GVFS.UnitTests/Virtualization/FileSystemCallbacksTests.cs b/GVFS/GVFS.UnitTests/Virtualization/FileSystemCallbacksTests.cs
index 2c7faeacb1..55c1ad296b 100644
--- a/GVFS/GVFS.UnitTests/Virtualization/FileSystemCallbacksTests.cs
+++ b/GVFS/GVFS.UnitTests/Virtualization/FileSystemCallbacksTests.cs
@@ -2,6 +2,7 @@
using GVFS.Common.NamedPipes;
using GVFS.Common.Tracing;
using GVFS.Tests.Should;
+using GVFS.UnitTests.Mock.Common;
using GVFS.UnitTests.Mock.Virtualization.Background;
using GVFS.UnitTests.Mock.Virtualization.BlobSize;
using GVFS.UnitTests.Mock.Virtualization.FileSystem;
@@ -11,8 +12,8 @@
using GVFS.Virtualization.Background;
using NUnit.Framework;
using System;
-using System.IO;
-
+using System.IO;
+
namespace GVFS.UnitTests.Virtualization
{
[TestFixture]
@@ -213,7 +214,7 @@ public void IsReadyForExternalAcquireLockRequests()
checkAvailabilityOnly: false,
parsedCommand: "git dummy-command"),
out denyMessage).ShouldBeFalse();
- denyMessage.ShouldEqual("Waiting for background operations to complete and for GVFS to release the lock");
+ denyMessage.ShouldEqual("Waiting for GVFS to release the lock");
backgroundTaskRunner.BackgroundTasks.Clear();
gitIndexProjection.ProjectionParseComplete = true;
@@ -301,6 +302,55 @@ public void FileAndFolderCallbacksScheduleBackgroundTasks()
}
}
+ [TestCase]
+ public void TestFileSystemOperationsInvalidateStatusCache()
+ {
+ using (MockBackgroundFileSystemTaskRunner backgroundTaskRunner = new MockBackgroundFileSystemTaskRunner())
+ using (MockFileSystemVirtualizer fileSystemVirtualizer = new MockFileSystemVirtualizer(this.Repo.Context, this.Repo.GitObjects))
+ using (MockGitIndexProjection gitIndexProjection = new MockGitIndexProjection(new[] { "test.txt" }))
+ using (MockGitStatusCache gitStatusCache = new MockGitStatusCache(this.Repo.Context, TimeSpan.Zero))
+ using (FileSystemCallbacks fileSystemCallbacks = new FileSystemCallbacks(
+ this.Repo.Context,
+ this.Repo.GitObjects,
+ RepoMetadata.Instance,
+ new MockBlobSizes(),
+ gitIndexProjection: gitIndexProjection,
+ backgroundFileSystemTaskRunner: backgroundTaskRunner,
+ fileSystemVirtualizer: fileSystemVirtualizer,
+ gitStatusCache: gitStatusCache))
+ {
+ this.ValidateActionInvalidatesStatusCache(backgroundTaskRunner, gitStatusCache, fileSystemCallbacks.OnFileConvertedToFull, "OnFileConvertedToFull.txt", FileSystemTask.OperationType.OnFileConvertedToFull);
+ this.ValidateActionInvalidatesStatusCache(backgroundTaskRunner, gitStatusCache, fileSystemCallbacks.OnFileCreated, "OnFileCreated.txt", FileSystemTask.OperationType.OnFileCreated);
+ this.ValidateActionInvalidatesStatusCache(backgroundTaskRunner, gitStatusCache, fileSystemCallbacks.OnFileDeleted, "OnFileDeleted.txt", FileSystemTask.OperationType.OnFileDeleted);
+ this.ValidateActionInvalidatesStatusCache(backgroundTaskRunner, gitStatusCache, fileSystemCallbacks.OnFileOverwritten, "OnFileDeleted.txt", FileSystemTask.OperationType.OnFileOverwritten);
+ this.ValidateActionInvalidatesStatusCache(backgroundTaskRunner, gitStatusCache, fileSystemCallbacks.OnFileSuperseded, "OnFileSuperseded.txt", FileSystemTask.OperationType.OnFileSuperseded);
+ this.ValidateActionInvalidatesStatusCache(backgroundTaskRunner, gitStatusCache, fileSystemCallbacks.OnFolderCreated, "OnFileSuperseded.txt", FileSystemTask.OperationType.OnFolderCreated);
+ this.ValidateActionInvalidatesStatusCache(backgroundTaskRunner, gitStatusCache, fileSystemCallbacks.OnFolderDeleted, "OnFileSuperseded.txt", FileSystemTask.OperationType.OnFolderDeleted);
+ this.ValidateActionInvalidatesStatusCache(backgroundTaskRunner, gitStatusCache, fileSystemCallbacks.OnFileConvertedToFull, "OnFileConvertedToFull.txt", FileSystemTask.OperationType.OnFileConvertedToFull);
+ }
+ }
+
+ private void ValidateActionInvalidatesStatusCache(
+ MockBackgroundFileSystemTaskRunner backgroundTaskRunner,
+ MockGitStatusCache gitStatusCache,
+ Action action,
+ string path,
+ FileSystemTask.OperationType operationType)
+ {
+ action(path);
+
+ backgroundTaskRunner.Count.ShouldEqual(1);
+ backgroundTaskRunner.BackgroundTasks[0].Operation.ShouldEqual(operationType);
+ backgroundTaskRunner.BackgroundTasks[0].VirtualPath.ShouldEqual(path);
+
+ backgroundTaskRunner.ProcessTasks();
+
+ gitStatusCache.InvalidateCallCount.ShouldEqual(1);
+
+ gitStatusCache.ResetCalls();
+ backgroundTaskRunner.BackgroundTasks.Clear();
+ }
+
private void CallbackSchedulesBackgroundTask(
MockBackgroundFileSystemTaskRunner backgroundTaskRunner,
Action callback,
diff --git a/GVFS/GVFS.VirtualFileSystemHook/GVFS.VirtualFileSystemHook.Windows.vcxproj b/GVFS/GVFS.VirtualFileSystemHook/GVFS.VirtualFileSystemHook.Windows.vcxproj
index c4ab5f1677..4ac353122a 100644
--- a/GVFS/GVFS.VirtualFileSystemHook/GVFS.VirtualFileSystemHook.Windows.vcxproj
+++ b/GVFS/GVFS.VirtualFileSystemHook/GVFS.VirtualFileSystemHook.Windows.vcxproj
@@ -23,13 +23,13 @@
Application
true
- v140
+ v141
MultiByte
Application
false
- v140
+ v141
true
MultiByte
@@ -123,4 +123,4 @@
-
\ No newline at end of file
+
diff --git a/GVFS/GVFS.Virtualization/Background/BackgroundFileSystemTaskRunner.cs b/GVFS/GVFS.Virtualization/Background/BackgroundFileSystemTaskRunner.cs
index 58082cf2c6..012ea805ec 100644
--- a/GVFS/GVFS.Virtualization/Background/BackgroundFileSystemTaskRunner.cs
+++ b/GVFS/GVFS.Virtualization/Background/BackgroundFileSystemTaskRunner.cs
@@ -71,8 +71,16 @@ public virtual int Count
get { return this.backgroundTasks.Count; }
}
+ public virtual void SetCallbacks(
+ Func preCallback,
+ Func callback,
+ Func postCallback)
+ {
+ throw new NotSupportedException("This method is only meant for unit tests, and must be implemented by test class if necessary for use in tests");
+ }
+
public virtual void Start()
- {
+ {
this.backgroundThread = Task.Factory.StartNew((Action)this.ProcessBackgroundTasks, TaskCreationOptions.LongRunning);
if (this.backgroundTasks.Count > 0)
{
diff --git a/GVFS/GVFS.Virtualization/FileSystem/FileSystemVirtualizer.cs b/GVFS/GVFS.Virtualization/FileSystem/FileSystemVirtualizer.cs
index 56b3b184e0..521b1aebb9 100644
--- a/GVFS/GVFS.Virtualization/FileSystem/FileSystemVirtualizer.cs
+++ b/GVFS/GVFS.Virtualization/FileSystem/FileSystemVirtualizer.cs
@@ -158,7 +158,7 @@ protected bool IsSpecialGitFile(string fileName)
fileName.Equals(GVFSConstants.SpecialGitFiles.GitIgnore, StringComparison.OrdinalIgnoreCase);
}
- protected void OnDotGitFileChanged(string relativePath)
+ protected void OnDotGitFileOrFolderChanged(string relativePath)
{
if (relativePath.Equals(GVFSConstants.DotGit.Index, StringComparison.OrdinalIgnoreCase))
{
@@ -168,6 +168,26 @@ protected void OnDotGitFileChanged(string relativePath)
{
this.FileSystemCallbacks.OnLogsHeadChange();
}
+ else if (IsPathHeadOrLocalBranch(relativePath))
+ {
+ this.FileSystemCallbacks.OnHeadOrRefChanged();
+ }
+ else if (relativePath.Equals(GVFSConstants.DotGit.Info.ExcludePath, StringComparison.OrdinalIgnoreCase))
+ {
+ this.FileSystemCallbacks.OnExcludeFileChanged();
+ }
+ }
+
+ protected void OnDotGitFileOrFolderDeleted(string relativePath)
+ {
+ if (IsPathHeadOrLocalBranch(relativePath))
+ {
+ this.FileSystemCallbacks.OnHeadOrRefChanged();
+ }
+ else if (relativePath.Equals(GVFSConstants.DotGit.Info.ExcludePath, StringComparison.OrdinalIgnoreCase))
+ {
+ this.FileSystemCallbacks.OnExcludeFileChanged();
+ }
}
protected EventMetadata CreateEventMetadata(
@@ -224,6 +244,18 @@ protected void LogUnhandledExceptionAndExit(string methodName, EventMetadata met
Environment.Exit(1);
}
+ private static bool IsPathHeadOrLocalBranch(string relativePath)
+ {
+ if (!relativePath.EndsWith(GVFSConstants.DotGit.LockExtension, StringComparison.OrdinalIgnoreCase) &&
+ (relativePath.Equals(GVFSConstants.DotGit.Head, StringComparison.OrdinalIgnoreCase) ||
+ relativePath.StartsWith(GVFSConstants.DotGit.Refs.Heads.RootFolder, StringComparison.OrdinalIgnoreCase)))
+ {
+ return true;
+ }
+
+ return false;
+ }
+
private void ExecuteFileOrNetworkRequest()
{
try
diff --git a/GVFS/GVFS.Virtualization/FileSystemCallbacks.cs b/GVFS/GVFS.Virtualization/FileSystemCallbacks.cs
index 77af2be643..5ef406b92f 100644
--- a/GVFS/GVFS.Virtualization/FileSystemCallbacks.cs
+++ b/GVFS/GVFS.Virtualization/FileSystemCallbacks.cs
@@ -41,7 +41,10 @@ public class FileSystemCallbacks : IDisposable, IHeartBeatMetadataProvider
private object postFetchJobLock;
private bool stopping;
- public FileSystemCallbacks(GVFSContext context, GVFSGitObjects gitObjects, RepoMetadata repoMetadata, FileSystemVirtualizer fileSystemVirtualizer)
+ private GitStatusCache gitStatusCache;
+ private bool enableGitStatusCache;
+
+ public FileSystemCallbacks(GVFSContext context, GVFSGitObjects gitObjects, RepoMetadata repoMetadata, FileSystemVirtualizer fileSystemVirtualizer, GitStatusCache gitStatusCache)
: this(
context,
gitObjects,
@@ -49,7 +52,8 @@ public FileSystemCallbacks(GVFSContext context, GVFSGitObjects gitObjects, RepoM
new BlobSizes(context.Enlistment.BlobSizesRoot, context.FileSystem, context.Tracer),
gitIndexProjection: null,
backgroundFileSystemTaskRunner: null,
- fileSystemVirtualizer: fileSystemVirtualizer)
+ fileSystemVirtualizer: fileSystemVirtualizer,
+ gitStatusCache: gitStatusCache)
{
}
@@ -60,7 +64,8 @@ public FileSystemCallbacks(
BlobSizes blobSizes,
GitIndexProjection gitIndexProjection,
BackgroundFileSystemTaskRunner backgroundFileSystemTaskRunner,
- FileSystemVirtualizer fileSystemVirtualizer)
+ FileSystemVirtualizer fileSystemVirtualizer,
+ GitStatusCache gitStatusCache = null)
{
this.logsHeadFileProperties = null;
this.postFetchJobLock = new object();
@@ -105,12 +110,29 @@ public FileSystemCallbacks(
placeholders,
this.modifiedPaths);
- this.backgroundFileSystemTaskRunner = backgroundFileSystemTaskRunner ?? new BackgroundFileSystemTaskRunner(
- this.context,
- this.PreBackgroundOperation,
- this.ExecuteBackgroundOperation,
- this.PostBackgroundOperation,
- Path.Combine(context.Enlistment.DotGVFSRoot, GVFSConstants.DotGVFS.Databases.BackgroundFileSystemTasks));
+ if (backgroundFileSystemTaskRunner != null)
+ {
+ this.backgroundFileSystemTaskRunner = backgroundFileSystemTaskRunner;
+ this.backgroundFileSystemTaskRunner.SetCallbacks(
+ this.PreBackgroundOperation,
+ this.ExecuteBackgroundOperation,
+ this.PostBackgroundOperation);
+ }
+ else
+ {
+ this.backgroundFileSystemTaskRunner = new BackgroundFileSystemTaskRunner(
+ this.context,
+ this.PreBackgroundOperation,
+ this.ExecuteBackgroundOperation,
+ this.PostBackgroundOperation,
+ Path.Combine(context.Enlistment.DotGVFSRoot, GVFSConstants.DotGVFS.Databases.BackgroundFileSystemTasks));
+ }
+
+ this.enableGitStatusCache = gitStatusCache != null;
+
+ // If the status cache is not enabled, create a dummy GitStatusCache that will never be initialized
+ // This lets us from having to add null checks to callsites into GitStatusCache.
+ this.gitStatusCache = gitStatusCache ?? new GitStatusCache(context, TimeSpan.Zero);
this.logsHeadPath = Path.Combine(this.context.Enlistment.WorkingDirectoryRoot, GVFSConstants.DotGit.Logs.Head);
@@ -153,6 +175,12 @@ public bool TryStart(out string error)
}
this.GitIndexProjection.Initialize(this.backgroundFileSystemTaskRunner);
+
+ if (this.enableGitStatusCache)
+ {
+ this.gitStatusCache.Initialize();
+ }
+
this.backgroundFileSystemTaskRunner.Start();
this.IsMounted = true;
@@ -168,6 +196,7 @@ public void Stop()
}
this.fileSystemVirtualizer.PrepareToStop();
+ this.gitStatusCache.Shutdown();
this.backgroundFileSystemTaskRunner.Shutdown();
this.GitIndexProjection.Shutdown();
this.BlobSizes.Shutdown();
@@ -201,6 +230,12 @@ public void Dispose()
this.modifiedPaths = null;
}
+ if (this.gitStatusCache != null)
+ {
+ this.gitStatusCache.Dispose();
+ this.gitStatusCache = null;
+ }
+
if (this.backgroundFileSystemTaskRunner != null)
{
this.backgroundFileSystemTaskRunner.Dispose();
@@ -224,7 +259,7 @@ public bool IsReadyForExternalAcquireLockRequests(NamedPipeMessages.LockData req
if (this.BackgroundOperationCount != 0)
{
- denyMessage = "Waiting for background operations to complete and for GVFS to release the lock";
+ denyMessage = "Waiting for GVFS to release the lock";
return false;
}
@@ -234,6 +269,11 @@ public bool IsReadyForExternalAcquireLockRequests(NamedPipeMessages.LockData req
return false;
}
+ if (!this.gitStatusCache.IsReadyForExternalAcquireLockRequests(requester, out denyMessage))
+ {
+ return false;
+ }
+
// Even though we're returning true and saying it's safe to ask for the lock
// there is no guarantee that the lock will be acquired, because GVFS itself
// could obtain the lock before the external holder gets it. Setting up an
@@ -270,6 +310,11 @@ public EventMetadata GetMetadataForHeartBeat(ref EventLevel eventLevel)
metadata.Add("ModifiedPathsCount", this.modifiedPaths.Count);
metadata.Add("PlaceholderCount", this.GitIndexProjection.EstimatedPlaceholderCount);
+ if (this.gitStatusCache.WriteTelemetryandReset(metadata))
+ {
+ eventLevel = EventLevel.Informational;
+ }
+
metadata.Add(nameof(RepoMetadata.Instance.EnlistmentId), RepoMetadata.Instance.EnlistmentId);
return metadata;
@@ -293,6 +338,7 @@ public virtual void OnIndexFileChange()
{
// Something wrote to the index without holding the GVFS lock, so we invalidate the projection
this.GitIndexProjection.InvalidateProjection();
+ this.InvalidateGitStatusCache();
// But this isn't something we expect to see, so log a warning
EventMetadata metadata = new EventMetadata
@@ -307,10 +353,24 @@ public virtual void OnIndexFileChange()
{
this.GitIndexProjection.InvalidateModifiedFiles();
this.backgroundFileSystemTaskRunner.Enqueue(FileSystemTask.OnIndexWriteWithoutProjectionChange());
+ this.InvalidateGitStatusCache();
}
else
{
this.GitIndexProjection.InvalidateProjection();
+ this.InvalidateGitStatusCache();
+ }
+ }
+
+ public void InvalidateGitStatusCache()
+ {
+ this.gitStatusCache.Invalidate();
+
+ // If there are background tasks queued up, then it will be
+ // refreshed after they have been processed.
+ if (this.backgroundFileSystemTaskRunner.Count == 0)
+ {
+ this.gitStatusCache.RefreshAsynchronously();
}
}
@@ -320,6 +380,20 @@ public virtual void OnLogsHeadChange()
this.logsHeadFileProperties = null;
}
+ public void OnHeadOrRefChanged()
+ {
+ this.InvalidateGitStatusCache();
+ }
+
+ ///
+ /// This method signals that the repository git exclude file
+ /// has been modified (i.e. .git/info/exclude)
+ ///
+ public void OnExcludeFileChanged()
+ {
+ this.InvalidateGitStatusCache();
+ }
+
public void OnFileCreated(string relativePath)
{
this.backgroundFileSystemTaskRunner.Enqueue(FileSystemTask.OnFileCreated(relativePath));
@@ -688,6 +762,7 @@ private FileSystemTaskResult TryAddModifiedPath(string virtualPath, bool isFolde
return isRetryable ? FileSystemTaskResult.RetryableError : FileSystemTaskResult.FatalError;
}
+ this.InvalidateGitStatusCache();
return FileSystemTaskResult.Success;
}
@@ -715,6 +790,7 @@ private FileSystemTaskResult AddModifiedPathAndRemoveFromPlaceholderList(string
private FileSystemTaskResult PostBackgroundOperation()
{
this.modifiedPaths.ForceFlush();
+ this.gitStatusCache.RefreshAsynchronously();
return this.GitIndexProjection.CloseIndex();
}
diff --git a/GVFS/GVFS.Virtualization/Projection/GitIndexProjection.cs b/GVFS/GVFS.Virtualization/Projection/GitIndexProjection.cs
index 862cffb3c9..c807016a85 100644
--- a/GVFS/GVFS.Virtualization/Projection/GitIndexProjection.cs
+++ b/GVFS/GVFS.Virtualization/Projection/GitIndexProjection.cs
@@ -325,7 +325,7 @@ public virtual void OnPlaceholderFileCreated(string virtualPath, string sha)
this.placeholderList.AddAndFlush(virtualPath, sha);
}
- public virtual bool TryGetProjectedItemsFromMemory(string folderPath, out IEnumerable projectedItems)
+ public virtual bool TryGetProjectedItemsFromMemory(string folderPath, out List projectedItems)
{
projectedItems = null;
@@ -376,7 +376,7 @@ public virtual ushort GetFilePathMode(string filePath)
}
}
- public virtual IEnumerable GetProjectedItems(
+ public virtual List GetProjectedItems(
CancellationToken cancellationToken,
BlobSizes.BlobSizesConnection blobSizesConnection,
string folderPath)
@@ -460,7 +460,7 @@ public virtual ProjectedFileInfo GetProjectedFileInfo(
return null;
}
- public FileSystemTaskResult OpenIndexForRead()
+ public virtual FileSystemTaskResult OpenIndexForRead()
{
if (!File.Exists(this.indexPath))
{
diff --git a/GVFS/GVFS/CommandLine/DehydrateVerb.cs b/GVFS/GVFS/CommandLine/DehydrateVerb.cs
index 52d90efb4d..39100557ab 100644
--- a/GVFS/GVFS/CommandLine/DehydrateVerb.cs
+++ b/GVFS/GVFS/CommandLine/DehydrateVerb.cs
@@ -151,7 +151,7 @@ private void CheckGitStatus(ITracer tracer, GVFSEnlistment enlistment)
isMounted = true;
GitProcess git = new GitProcess(enlistment);
- statusResult = git.Status(allowObjectDownloads: false);
+ statusResult = git.Status(allowObjectDownloads: false, useStatusCache: false);
if (statusResult.HasErrors)
{
return false;
diff --git a/GVFS/GVFS/CommandLine/GVFSVerb.cs b/GVFS/GVFS/CommandLine/GVFSVerb.cs
index 75a369424f..13464833cc 100644
--- a/GVFS/GVFS/CommandLine/GVFSVerb.cs
+++ b/GVFS/GVFS/CommandLine/GVFSVerb.cs
@@ -62,6 +62,17 @@ public static bool TrySetRequiredGitConfigSettings(Enlistment enlistment)
string expectedHooksPath = Path.Combine(enlistment.WorkingDirectoryRoot, GVFSConstants.DotGit.Hooks.Root);
expectedHooksPath = Paths.ConvertPathToGitFormat(expectedHooksPath);
+ string gitStatusCachePath = null;
+ if (!GVFSEnlistment.IsUnattended(tracer: null) && GVFSPlatform.Instance.IsGitStatusCacheSupported())
+ {
+ gitStatusCachePath = Path.Combine(
+ enlistment.EnlistmentRoot,
+ GVFSConstants.DotGVFS.Root,
+ GVFSConstants.DotGVFS.GitStatusCache.CachePath);
+
+ gitStatusCachePath = Paths.ConvertPathToGitFormat(gitStatusCachePath);
+ }
+
// These settings are required for normal GVFS functionality.
// They will override any existing local configuration values.
Dictionary requiredSettings = new Dictionary
@@ -90,6 +101,7 @@ public static bool TrySetRequiredGitConfigSettings(Enlistment enlistment)
{ "index.version", "4" },
{ "merge.stat", "false" },
{ "receive.autogc", "false" },
+ { "status.deserializePath", gitStatusCachePath },
};
if (!TrySetConfig(enlistment, requiredSettings, isRequired: true))
@@ -545,13 +557,23 @@ private static bool TrySetConfig(Enlistment enlistment, Dictionary setting in configSettings)
{
GitConfigSetting existingSetting;
- if (!existingConfigSettings.TryGetValue(setting.Key, out existingSetting) ||
- (isRequired && !existingSetting.HasValue(setting.Value)))
+ if (setting.Value != null)
{
- GitProcess.Result setConfigResult = git.SetInLocalConfig(setting.Key, setting.Value);
- if (setConfigResult.HasErrors)
+ if (!existingConfigSettings.TryGetValue(setting.Key, out existingSetting) ||
+ (isRequired && !existingSetting.HasValue(setting.Value)))
{
- return false;
+ GitProcess.Result setConfigResult = git.SetInLocalConfig(setting.Key, setting.Value);
+ if (setConfigResult.HasErrors)
+ {
+ return false;
+ }
+ }
+ }
+ else
+ {
+ if (existingConfigSettings.TryGetValue(setting.Key, out existingSetting))
+ {
+ git.DeleteFromLocalConfig(setting.Key);
}
}
}
diff --git a/GVFS/GVFS/CommandLine/PrefetchVerb.cs b/GVFS/GVFS/CommandLine/PrefetchVerb.cs
index 67d209f626..aa9d1c1be8 100644
--- a/GVFS/GVFS/CommandLine/PrefetchVerb.cs
+++ b/GVFS/GVFS/CommandLine/PrefetchVerb.cs
@@ -219,7 +219,7 @@ private void PrefetchCommits(ITracer tracer, GVFSEnlistment enlistment, GitObjec
{
success = this.ShowStatusWhileRunning(
() => CommitPrefetcher.TryPrefetchCommitsAndTrees(tracer, enlistment, fileSystem, gitObjects, out error),
- "Fetching commits and trees " + this.GetCacheServerDisplay(cacheServer));
+ "Fetching commits and trees " + this.GetCacheServerDisplay(cacheServer, enlistment.RepoUrl));
}
if (!success)
@@ -310,7 +310,7 @@ private void PrefetchBlobs(ITracer tracer, GVFSEnlistment enlistment, GitObjects
this.HydrateFiles
? "Fetching blobs and hydrating files "
: "Fetching blobs ";
- this.ShowStatusWhileRunning(doPrefetch, message + this.GetCacheServerDisplay(cacheServer));
+ this.ShowStatusWhileRunning(doPrefetch, message + this.GetCacheServerDisplay(cacheServer, enlistment.RepoUrl));
}
if (blobPrefetcher.HasFailures)
@@ -352,9 +352,9 @@ private bool CheckIsMounted(bool verbose)
}
}
- private string GetCacheServerDisplay(CacheServerInfo cacheServer)
+ private string GetCacheServerDisplay(CacheServerInfo cacheServer, string repoUrl)
{
- if (cacheServer.Name != null && !cacheServer.Name.Equals(CacheServerInfo.ReservedNames.None))
+ if (!cacheServer.IsNone(repoUrl))
{
return "from cache server";
}
diff --git a/GVFS/GVFS/packages.config b/GVFS/GVFS/packages.config
index 28f0c5ddbb..e2bc43412c 100644
--- a/GVFS/GVFS/packages.config
+++ b/GVFS/GVFS/packages.config
@@ -1,7 +1,7 @@
-
+
-
+
@@ -12,4 +12,4 @@
-
\ No newline at end of file
+
diff --git a/GitHooksLoader/GitHooksLoader.vcxproj b/GitHooksLoader/GitHooksLoader.vcxproj
index 6fb2d7218b..dce7265ec4 100644
--- a/GitHooksLoader/GitHooksLoader.vcxproj
+++ b/GitHooksLoader/GitHooksLoader.vcxproj
@@ -1,4 +1,4 @@
-
+
@@ -21,13 +21,13 @@
Application
true
- v140
+ v141
Unicode
Application
false
- v140
+ v141
true
Unicode
@@ -115,4 +115,4 @@
-
\ No newline at end of file
+
diff --git a/MirrorProvider/MirrorProvider.Windows/ActiveEnumeration.cs b/MirrorProvider/MirrorProvider.Windows/ActiveEnumeration.cs
index 8584734dd3..ed6d4197c7 100644
--- a/MirrorProvider/MirrorProvider.Windows/ActiveEnumeration.cs
+++ b/MirrorProvider/MirrorProvider.Windows/ActiveEnumeration.cs
@@ -1,19 +1,17 @@
using ProjFS;
-using System;
using System.Collections.Generic;
namespace MirrorProvider.Windows
{
- public class ActiveEnumeration : IDisposable
+ public class ActiveEnumeration
{
- private readonly IEnumerable fileInfos;
-
- private IEnumerator fileInfoEnumerator;
+ // Use our own enumerator to avoid having to dispose anything
+ private ProjectedFileInfoEnumerator fileInfoEnumerator;
private string filterString = null;
- public ActiveEnumeration(IEnumerable fileInfos)
+ public ActiveEnumeration(List fileInfos)
{
- this.fileInfos = fileInfos;
+ this.fileInfoEnumerator = new ProjectedFileInfoEnumerator(fileInfos);
this.ResetEnumerator();
this.MoveNext();
}
@@ -94,15 +92,6 @@ public string GetFilterString()
return this.filterString;
}
- public void Dispose()
- {
- if (this.fileInfoEnumerator != null)
- {
- this.fileInfoEnumerator.Dispose();
- this.fileInfoEnumerator = null;
- }
- }
-
private void SaveFilter(string filter)
{
if (string.IsNullOrEmpty(filter))
@@ -126,7 +115,43 @@ private bool IsCurrentHidden()
private void ResetEnumerator()
{
- this.fileInfoEnumerator = this.fileInfos.GetEnumerator();
+ this.fileInfoEnumerator.Reset();
+ }
+
+ private class ProjectedFileInfoEnumerator
+ {
+ private List list;
+ private int index;
+
+ public ProjectedFileInfoEnumerator(List projectedFileInfos)
+ {
+ this.list = projectedFileInfos;
+ this.Reset();
+ }
+
+ public ProjectedFileInfo Current { get; private set; }
+
+ // Combination of the logic in List.Enumerator MoveNext() and MoveNextRare()
+ // https://github.com/dotnet/corefx/blob/b492409b4a1952cda4b078f800499d382e1765fc/src/Common/src/CoreLib/System/Collections/Generic/List.cs#L1137
+ public bool MoveNext()
+ {
+ if (this.index < this.list.Count)
+ {
+ this.Current = this.list[this.index];
+ this.index++;
+ return true;
+ }
+
+ this.index = this.list.Count + 1;
+ this.Current = null;
+ return false;
+ }
+
+ public void Reset()
+ {
+ this.index = 0;
+ this.Current = null;
+ }
}
}
}
diff --git a/MirrorProvider/MirrorProvider.Windows/WindowsFileSystemVirtualizer.cs b/MirrorProvider/MirrorProvider.Windows/WindowsFileSystemVirtualizer.cs
index 454cc17077..f89d1b0d86 100644
--- a/MirrorProvider/MirrorProvider.Windows/WindowsFileSystemVirtualizer.cs
+++ b/MirrorProvider/MirrorProvider.Windows/WindowsFileSystemVirtualizer.cs
@@ -60,11 +60,11 @@ private HResult StartDirectoryEnumeration(int commandId, Guid enumerationId, str
// what is on disk, and it assumes that both lists are already sorted.
ActiveEnumeration activeEnumeration = new ActiveEnumeration(
this.GetChildItems(relativePath)
- .OrderBy(file => file.Name, StringComparer.OrdinalIgnoreCase));
+ .OrderBy(file => file.Name, StringComparer.OrdinalIgnoreCase)
+ .ToList());
if (!this.activeEnumerations.TryAdd(enumerationId, activeEnumeration))
{
- activeEnumeration.Dispose();
return HResult.InternalError;
}
@@ -76,11 +76,7 @@ private HResult EndDirectoryEnumeration(Guid enumerationId)
Console.WriteLine($"EndDirectioryEnumeration: {enumerationId}");
ActiveEnumeration activeEnumeration;
- if (this.activeEnumerations.TryRemove(enumerationId, out activeEnumeration))
- {
- activeEnumeration.Dispose();
- }
- else
+ if (!this.activeEnumerations.TryRemove(enumerationId, out activeEnumeration))
{
return HResult.InternalError;
}
diff --git a/Readme.md b/Readme.md
index 25ca415c06..6371eb155f 100644
--- a/Readme.md
+++ b/Readme.md
@@ -27,9 +27,7 @@ If you'd like to build your own VFS for Git Windows installer:
* .NET Core cross-platform development
* Include the following additional components:
* .NET Core runtime
- * .NET Framework 3.5 development tools
* C++/CLI support
- * VC++ 2015.3 v140 toolset
* Windows 10 SDK (10.0.10240.0)
* Install the .NET Core 2.1 SDK (https://www.microsoft.com/net/download/dotnet-core/2.1)
* Create a folder to clone into, e.g. `C:\Repos\VFSForGit`
diff --git a/Scripts/Mac/BuildGVFSForMac.sh b/Scripts/Mac/BuildGVFSForMac.sh
index b150a84aa1..5078f9cb06 100755
--- a/Scripts/Mac/BuildGVFSForMac.sh
+++ b/Scripts/Mac/BuildGVFSForMac.sh
@@ -5,11 +5,11 @@ if [ -z $CONFIGURATION ]; then
CONFIGURATION=Debug
fi
-SCRIPTDIR=$(dirname ${BASH_SOURCE[0]})
+SCRIPTDIR="$(dirname ${BASH_SOURCE[0]})"
# convert to an absolute path because it is required by `dotnet publish`
pushd $SCRIPTDIR
-SCRIPTDIR=$(pwd)
+SCRIPTDIR="$(pwd)"
popd
SRCDIR=$SCRIPTDIR/../..
@@ -26,24 +26,18 @@ PACKAGES=$ROOTDIR/packages
# Build the ProjFS kext and libraries
$SRCDIR/ProjFS.Mac/Scripts/Build.sh $CONFIGURATION || exit 1
-# Build GVFS
-
-# Until GVFS.PreBuild is fully set up for MacOS, generate GitVersionConstants here.
+# Create the directory where we'll do pre build tasks
BUILDDIR=$BUILDOUTPUT/GVFS.Build
if [ ! -d $BUILDDIR ]; then
mkdir $BUILDDIR || exit 1
fi
-echo '// This file is auto-generated by GVFS.PreBuild.GenerateGitVersionConstants. Any changes made directly in this file will be lost.
-using GVFS.Common.Git;
-
-namespace GVFS.Common
-{
- public static partial class GVFSConstants
- {
- public static readonly GitVersion SupportedGitVersion = new GitVersion(2, 17, 0, "gvfs", 1, 123);
- }
-}' > $BUILDDIR/GVFSConstants.GitVersion.cs || exit 1
+$SCRIPTDIR/DownloadGVFSGit.sh || exit 1
+GVFSPROPS=$SRCDIR/GVFS/GVFS.Build/GVFS.props
+GITVERSION="$(cat $GVFSPROPS | grep GitPackageVersion | grep -Eo '[0-9.]{1,}')"
+GITPATH="$(find $PACKAGES/gitformac.gvfs.installer/$GITVERSION -type f -name *.dmg)" || exit 1
+# Now that we have a path containing the version number, generate GVFSConstants.GitVersion.cs
+$SCRIPTDIR/GenerateGitVersionConstants.sh "$GITPATH" $BUILDDIR || exit 1
DOTNETCONFIGURATION=$CONFIGURATION.Mac
dotnet restore $SRCDIR/GVFS.sln /p:Configuration=$DOTNETCONFIGURATION --packages $PACKAGES || exit 1
diff --git a/Scripts/Mac/CleanupFunctionalTests.sh b/Scripts/Mac/CleanupFunctionalTests.sh
index e32fb03756..dc30be675e 100755
--- a/Scripts/Mac/CleanupFunctionalTests.sh
+++ b/Scripts/Mac/CleanupFunctionalTests.sh
@@ -1,5 +1,11 @@
+SCRIPTDIR=$(dirname ${BASH_SOURCE[0]})
+SRCDIR=$SCRIPTDIR/../..
+$SRCDIR/ProjFS.Mac/Scripts/UnloadPrjFSKext.sh
+
+sudo rm -r /GVFS.FT
+
PATURL=$1
if [[ -z $PATURL ]] ; then
exit 1
fi
-security delete-generic-password -s "gcm4ml:git:$PATURL"
+security delete-generic-password -s "gcm4ml:git:$PATURL"
\ No newline at end of file
diff --git a/Scripts/Mac/DownloadGVFSGit.sh b/Scripts/Mac/DownloadGVFSGit.sh
new file mode 100755
index 0000000000..3d1ced9c96
--- /dev/null
+++ b/Scripts/Mac/DownloadGVFSGit.sh
@@ -0,0 +1,9 @@
+SCRIPTDIR="$(dirname ${BASH_SOURCE[0]})"
+SRCDIR=$SCRIPTDIR/../..
+BUILDDIR=$SRCDIR/../BuildOutput/GVFS.Build
+PACKAGESDIR=$SRCDIR/../packages
+GVFSPROPS=$SRCDIR/GVFS/GVFS.Build/GVFS.props
+GITVERSION="$(cat $GVFSPROPS | grep GitPackageVersion | grep -Eo '[0-9.]{1,}')"
+cp $SRCDIR/nuget.config $BUILDDIR
+dotnet new classlib -n GVFS.Restore -o $BUILDDIR --force
+dotnet add $BUILDDIR/GVFS.Restore.csproj package --package-directory $PACKAGESDIR GitForMac.GVFS.Installer --version $GITVERSION
\ No newline at end of file
diff --git a/Scripts/Mac/GenerateGitVersionConstants.sh b/Scripts/Mac/GenerateGitVersionConstants.sh
new file mode 100755
index 0000000000..559571b1c7
--- /dev/null
+++ b/Scripts/Mac/GenerateGitVersionConstants.sh
@@ -0,0 +1,16 @@
+VERSIONREGEX="([[:digit:]]+).([[:digit:]]+).([[:digit:]]+).([[:alpha:]]+).([[:digit:]]+).([[:digit:]]+)"
+if [[ $1 =~ $VERSIONREGEX ]]
+then
+ cat >$2/GVFSConstants.GitVersion.cs <&1 | egrep -o '"[[:digit:]]+.[[:digit:]]+.[[:digit:]]+"' | xargs)"
if [[ -z $JAVAVERSION ]]; then
- JAVAVERSION="10.0.1"
+ JAVAVERSION="10.0.2"
fi
# Work around https://github.com/Microsoft/Git-Credential-Manager-for-Mac-and-Linux/issues/71
-git config --global credential.helper "!/Library/Java/JavaVirtualMachines/jdk-$JAVAVERSION.jdk/Contents/Home/bin/java -Ddebug=false --add-modules java.xml.bind -Djava.net.useSystemProxies=true -jar /usr/local/Cellar/git-credential-manager/2.0.3/libexec/git-credential-manager-2.0.3.jar"
+git config --global credential.helper "!/Library/Java/JavaVirtualMachines/jdk-$JAVAVERSION.jdk/Contents/Home/bin/java -Ddebug=false --add-modules java.xml.bind -Djava.net.useSystemProxies=true -jar /usr/local/Cellar/git-credential-manager/2.0.3/libexec/git-credential-manager-2.0.3.jar" || exit 1
# If we're running on an agent where the PAT environment variable is set and a URL is passed into the script, add it to the keychain.
PAT=$SYSTEM_ACCESSTOKEN
PATURL=$1
if [[ ! -z $PAT && ! -z $PATURL ]] ; then
- security add-generic-password -a "Personal Access Token" -s "gcm4ml:git:$PATURL" -D Credential -w $PAT
+ security add-generic-password -a "Personal Access Token" -s "gcm4ml:git:$PATURL" -D Credential -w $PAT || exit 1
fi