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 <